{"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":"fd2b803a-4477-4ac6-9d75-a5c1c16df44e","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"8ef8929d2ef9e98ed9898cb363f66caedf958040b800a1c5c49bf98ee49c0e6a","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":"4be1b93b8c24334d03c388666ec6abcfa2b7ede74e2f80bebafcffaf96a9a01c","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":"a9a54e2b816d9da0ed41cae4ffb3ea6e2c4c1b973638f0b3576860b61d21809a","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":"c334d7227b758c9612ed9cbb93f4f816fc02b9c86805c493d111bdbc983d1f69","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":"2863ccf3f489c4819cac27ec6df1995534976f18168ab610aba55db29e2d8320","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":"073b86f1f2ccc86346aa669e54e36266d0cbb55d103dbb19e7efaeb12183da74","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"836710b9b89d0424e7c8b68a6db933f4c0fb0a61cca155f085e4393e5cd7613f","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"6f50f51236405f70b2512eb153c13dbfce93966ca8ba3655a652f7cf518d53a7","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":"Build and deploy PinballEscrow for Space Pinball, the NIGHTSHIFT player-versus-player skill game. Two players stake the same amount of the same token; the better player, as decided by the NIGHTSHIFT server's deterministic replay of both games, takes the pot minus a 5% fee.\n\nTokens: only IMD (0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7) and $SHIFT (0xe5e58e53d0bb204aa62c995a6c8cc8a48e60c3ac), both plain ERC-20 with 18 decimals and no transfer fee. Stakes allowed per token are fixed at deploy: IMD 1, 5, 10, 25; SHIFT 100000, 500000, 1000000, 5000000 (whole tokens).\n\nFlow:\n1. open(bytes32 matchId, address token, uint256 stake): player A deposits the stake (transferFrom, exact amount). Emits Opened(matchId, token, stake, playerA).\n2. join(bytes32 matchId): player B deposits the same token and stake. Emits Joined(matchId, playerB). A match has exactly two players; A cannot join her own match.\n3. settle(bytes32 matchId, address winner, bool draw, uint256 deadline, bytes signature): anyone may submit the result signed by the result signer (EIP-712 typed data: domain name \"SpacePinball\", version \"1\", chainId, this contract; struct Result(bytes32 matchId, address winner, bool draw, uint256 deadline)). The signature must be valid, not expired, and for a joined, unsettled match. Winner gets 95% of the pot; 5% goes to the fee splitter 0x1119d5df2f046bc2d3c0f6279ef2e9ce49422d44. A draw refunds both players in full. Emits Settled(matchId, winner, draw, payout, fee).\n4. refund(bytes32 matchId): if a match is not joined within 1 hour of open, A gets her stake back; if a joined match is not settled within 24 hours, both get their stake back. Emits Refunded(matchId).\n\nOwner: 0x6c4c78f2c3bdf4c4318fe305fb1c8bc1694d19b5. The owner can only change the result signer (setSigner, with an event) and pause new opens (pause/unpause; settle and refund keep working while paused). The owner can never move, take or redirect any stake, change the fee (fixed 5%), the fee recipient, the tokens or the stake list. No upgradeability, no delegatecall, no selfdestruct. Reentrancy-safe (checks-effects-interactions plus a guard), uses SafeERC20, every state change emits an event. Initial result signer: set by constructor argument, equal to $owner.\n\nTests (Foundry): full flows for both tokens, draw, both refunds, wrong signer, expired and replayed signatures, double settle, joining your own match, wrong stake, paused opens, fee exactly 5% to the splitter, fuzz on stakes and orders of calls.","parentJobId":null,"planHash":"ba53b4468a1f7a6d3bf400fd1f29b50a7513049e71970699524e922dc7fe015e","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"fd2b803a-4477-4ac6-9d75-a5c1c16df44e","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2193-build-deploy-pinballescrow-space"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51232","feedbackHash":"95d07f6df96d4797bf139450a4ee3140a83c1b2a32be6cd573aa2d52abd1fcf4","nodeKey":"audit_economics","submissionHash":"8ef8929d2ef9e98ed9898cb363f66caedf958040b800a1c5c49bf98ee49c0e6a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52131","feedbackHash":"ff42a5f7ca7b73caf63c6a438755afc3a6a31ed28d2a1ab3a980ee08b6e6c753","nodeKey":"audit_flow","submissionHash":"4be1b93b8c24334d03c388666ec6abcfa2b7ede74e2f80bebafcffaf96a9a01c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52660","feedbackHash":"f040be5897fcec56f57cba263b769ba0bb4677e73f0cc2983b518885b7ed3f2f","nodeKey":"audit_judge","submissionHash":"f58deab04cddf4022c4f617d052c750725a9c184d34e65f18808ee33995defc6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51166","feedbackHash":"13aab6dfa8d51d89e97b0f1d46a155a4212557ec9a5af61eb5f68fda0dee9baa","nodeKey":"audit_judge","submissionHash":"a9a54e2b816d9da0ed41cae4ffb3ea6e2c4c1b973638f0b3576860b61d21809a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51487","feedbackHash":"857de85f1762792af864b547c3f54be3659f3402a16557a9cbfb29f151e28da5","nodeKey":"audit_math","submissionHash":"c334d7227b758c9612ed9cbb93f4f816fc02b9c86805c493d111bdbc983d1f69","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52164","feedbackHash":"457eb047c903b792a52fa6c37be61c7ae3fab8f356816b7dd1ed49802994050f","nodeKey":"audit_permissions","submissionHash":"2863ccf3f489c4819cac27ec6df1995534976f18168ab610aba55db29e2d8320","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52256","feedbackHash":"2b9f36127bb606d833ef48244e032318e713c77f772d08d0f6efc8c8c880997d","nodeKey":"build_contract_project","submissionHash":"073b86f1f2ccc86346aa669e54e36266d0cbb55d103dbb19e7efaeb12183da74","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"53234","feedbackHash":"d92fd29d6a02f2268127c55dcdcdeca3386c973f5d864a04844011cf0d6d1abb","nodeKey":"build_contract_project","submissionHash":"b29525166dcdeb1396f98d8b41e2dcdc0bd4a8ae64c908a1a33d9ccffafc4828","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52665","feedbackHash":"f5d4c6195b207f9cb592f091ab00cb5a1b87ee481854fe0bbfdbe2e2b1e6a6f0","nodeKey":"manifest","submissionHash":"836710b9b89d0424e7c8b68a6db933f4c0fb0a61cca155f085e4393e5cd7613f","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52252","feedbackHash":"c16007df8aeb21b3a5ea1f9627668a829aa2d3c3bb960179e6981e83645b3d5c","nodeKey":"manifest","submissionHash":"b967ca41d0250c79f1618308f402c35dc5d78d7cb595187eebab7bf8588457f0","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51757","feedbackHash":"638e69b42269d85b954ffdfff3bdb7ab6cc7acb12f1a2add4cbe6c7e5907297e","nodeKey":"write_foundry_tests","submissionHash":"c1516214714a1bc10fcdee292856a347228a0872a9eec3b6c8a81496725a8299","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52149","feedbackHash":"4323228a6b0025b52efdb3dbcbd9849b92e8e3abd6ff6e5e301292c75892204d","nodeKey":"write_foundry_tests","submissionHash":"6f50f51236405f70b2512eb153c13dbfce93966ca8ba3655a652f7cf518d53a7","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"82e129d71f2198b55510b13b5cdf1cc320f3d23c0ad1e6a33f97e2b31f7cb34e","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"ae0ed9561aa4f9daedbb73b614e26f8ab4d53684159dce05c353dbf9c66266fd","device":"72f49cf84b9ab056","findings":[],"hash":"073b86f1f2ccc86346aa669e54e36266d0cbb55d103dbb19e7efaeb12183da74","nodeId":"45aa6ab3-8c75-45cd-99dd-5c5b535da8f8","outcome":"completed","summary":"Revision complete, with one important caveat: the shell sandbox on this machine refused every command for the whole session (the wrapper failed with `apply-seccomp: write /proc/self/setgroups ... Permission denied` before any command started), so I could not run `forge build`, `forge test` or `forge fmt --check` for this revision. All changes were made with the file tools and checked by hand against the compiler rules and the accepted suite's conventions. The validation record states this plainly.\n\n**Finding a257ef2d (high, owner) — fixed.** The owner was a hard-coded constant and the constructor reverted for any initial signer other than that constant, so a launch whose `$owner` differed could neither deploy nor have `owner()` follow the manifest. The constructor is now `(address owner_, address initialSigner)`, `owner` is an immutable set from the argument, both must be non-zero, and the manifest fills both with `$owner`. A new fuzz test deploys through a factory contract with a pranked origin and checks that `owner()` equals the argument and that neither factory nor origin can administer the contract.\n\n**Finding cc82eb10 (medium, ID front-running) — fixed.** Both reviewer scenarios reproduced by inspection. The brief's `open` and `join` signatures are unchanged; two guarded entry points were added:\n- `joinChecked(matchId, token, stake, playerA)` reverts `MatchMismatch` unless the ID holds exactly the agreed terms, so a squatted ID cannot bind B to another tier or opponent.\n- `openInvite(matchId, token, stake, invitee)` records the arranged opponent in a new `invitee` field and emits `Invited`; any other joiner reverts `NotInvited`, closing the slot-snipe.\n\nSeven new unit and fuzz tests cover the reviewer's squat and snipe scenarios, each mismatch field, invalid invitees, and invite-only refunds and pausing. The invariant handler models the invitee and drives both new actions with every mismatch kind.\n\n**Finding 93ef1f4d (info, signer revival) — disputed.** It reproduces, but the brief fixes the Result struct and the only struct-free mitigation would forbid the owner from ever re-selecting a key. It stays a documented operational rule in the README and security review, as the finding itself suggested.\n\nFiles changed: `src/PinballEscrow.sol`, the four test files under `test/`, `README.md`, `docs/security-review.md`, `docs/validation.md`, and the new `.imd-responses.json`. The verifier's build and test run with the given fuzz seed is the authoritative result for this round.","treeHash":"d281ca822ed8bfd31ce4f3e5175ba76c14fd5036","usage":{"cachedInputTokens":3315862,"inputTokens":994,"model":"claude-fable-5-1","outputTokens":51272,"runtime":"claude","turns":70,"wallClockMs":792835}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"63458a1bd2b2d376","findings":[{"citation":"resolved","description":"Trust-gap / asymmetry finding (access x asymmetry). open() reserves any bytes32 ID for whoever lands first (line 102: `if (_matches[matchId].state != State.None) revert MatchAlreadyExists();`) and binds the token and stake to that ID. join(bytes32) takes no token or stake argument and deposits whatever the stored match says (line 131). The two sides are asymmetric: the opener chooses token and stake explicitly, the joiner only names an ID. When match IDs are assigned off-chain and communicated to both players (the brief's flow), an observer of the mempool can front-run player A's open() with the same ID but a different, larger token/stake tier. A's open reverts with MatchAlreadyExists, and B's join(id), if B holds a standing allowance for the other token, pulls the squatter's tier (up to 5,000,000 SHIFT) instead of the 1 IMD B expected, and locks it in a match against the squatter. The squatter cannot take B's funds (settle still needs the signer and the winner must be a recorded player; refund returns stakes to depositors), so the impact is denial of the intended match and a forced lock-up of up to 24 hours of a tier and token B did not choose, repeatable per match at the cost of gas plus the squatter's own stake being locked for the same period. The brief fixes the join(bytes32) signature and public open, so the author documented this as a client/server obligation (README 'Clients must also confirm the recorded token, stake and participants'). It is reported here because the on-chain contract cannot protect B, and because the fix is cheap if the interface may change: either derive the match key from (msg.sender, matchId) in open() so only A can reserve her own ID, or give join() the expected token and stake and revert on mismatch (join(bytes32 matchId, address token, uint256 stake)). If the interface must stay, the frontend must approve only the exact stake per call, never a standing max allowance, and verify matches(id) in the same block before joining; that mitigation is off-chain and is a trust assumption, not enforcement.","line":131,"path":"src/PinballEscrow.sol","reproduction":"State: fresh deployment; alice, bob, mallory each hold IMD and SHIFT and have approved the escrow for type(uint256).max on both tokens (common wallet default). Off-chain, the server tells alice and bob to use id = keccak256('match 42') for a 1 IMD game. Calls: (1) mallory: open(id, SHIFT, 5_000_000e18) is mined first (front-run of alice's pending tx). (2) alice: open(id, IMD, 1e18) -> reverts MatchAlreadyExists. (3) bob: join(id) -> succeeds; bob's SHIFT balance drops by 5,000,000e18 and matches(id).playerA == mallory, playerB == bob, token == SHIFT. Expected by bob: a 1 IMD stake against alice, or a revert. Actual: 5,000,000 SHIFT pulled into a match against mallory. (4) refund(id) reverts RefundNotReady until joinedAt + 24 hours; after that both stakes return. Verified with test/scratch/Squat.t.sol (passes on current code, demonstrating the behaviour).","severity":"low","snippet":"        _deposit(game.token, msg.sender, game.stake);","title":"First-come match IDs let a front-runner squat an invited match and make join() pull a token and stake the joiner never agreed to"},{"citation":"resolved","description":"Trust-gap note (access x asymmetry, admin setter altering in-flight authorization). settle() accepts any signature from the *current* signer over Result(matchId, winner, draw, deadline); the signed type carries no signer identity, nonce or epoch, and setSigner() only overwrites the single `signer` slot. Rotating away from a key invalidates its outstanding signatures (tested in test_SignerRotationInvalidatesOutstandingOldKeyResults), but rotating back to that key, or to any address that once signed, re-enables every signature it ever produced whose deadline has not passed and whose match is still Joined and inside its 24-hour settlement window. A result signed by a compromised key is therefore only suspended, not revoked, while the compromised address is deselected. Harm requires the owner to reinstate a key that produced bad signatures, so there is no unprivileged amplifier and no revision is requested; the README already warns against reusing a retired key as a revocation strategy. Recorded so the owner's operational rule is explicit: treat signer rotation as permanent for any key that may have signed a disputed result, or, if the brief may change, include the signer address or an epoch counter in the signed struct so old signatures die with the rotation.","line":190,"path":"src/PinballEscrow.sol","reproduction":"State: signer = K1 (vm.addr(0xa11ce)); match ID joined at t0 between alice and bob. Calls: (1) K1 signs Result(ID, bob, false, deadline = t0 + 20 hours) -> sigK1. (2) owner: setSigner(K2). (3) relayer: settle(ID, bob, false, deadline, sigK1) -> reverts InvalidSignature (expected). (4) owner: setSigner(K1) at t0 + 1 hour. (5) relayer: settle(ID, bob, false, deadline, sigK1) -> succeeds, bob receives 95% of pot. Expected if rotation were a revocation: step 5 reverts. Actual: step 5 settles with the pre-rotation signature.","severity":"info","snippet":"        signer = newSigner;","title":"setSigner has no signer epoch, so re-selecting a previously retired key revives its unexpired result signatures for still-joined matches"}],"hash":"2863ccf3f489c4819cac27ec6df1995534976f18168ab610aba55db29e2d8320","nodeId":"04facdf7-3fb5-4185-9145-6e27d5dc5ef1","outcome":"completed","summary":"The review is complete. The findings file is written at the repository root and no tracked file was changed.\n\n**Outcome: no critical, high or medium defects in the Access Control, Trust Gap or Asymmetry areas.** PinballEscrow's permission model is exactly what the brief asked for. The owner is a constant with only `setSigner`, `pause` and `unpause`. Fee, splitter, tokens and tiers are constants. Join, settle and refund keep working while paused. The settle and refund windows are exact complements of the join and settle windows, so no match can be left with neither path available. State is set before every token transfer, every path is guarded, and the EIP-712 digest binds chain, contract and match ID. All 56 existing tests pass and formatting is clean.\n\n**Two findings recorded, both with confirmed reproductions:**\n\n- **Low, match-ID squatting.** Any caller can reserve any match ID first, and `join(bytes32)` deposits whatever token and stake the stored match carries. A front-runner can replace an invited 1 IMD match with a 5,000,000 SHIFT one; the invited joiner with a standing allowance then has that amount pulled and locked up to 24 hours against the squatter. No funds can be taken, so this is griefing and a forced lock-up. The brief fixes the `join` signature, so I framed the fix as a scope decision (bind the ID to the opener, or add token and stake to join) with the off-chain mitigation named.\n- **Info, signer reinstatement.** Rotating the signer away suspends a key's signatures, but rotating back to it revives any unexpired ones for still-joined matches. It needs the owner to reinstate a bad key, so it is documented as an operational rule rather than a defect.\n\n**Verified beyond the code:** I probed both live mainnet tokens. IMD and SHIFT moved exact amounts through open, join and settle on a fork at the current head, and neither exposes tax, blocklist, pause or proxy selectors, so the plain-ERC-20 assumption holds on chain today.\n\n**Coverage:** all seven entry points have rows, plus four invariant rows covering owner powers, constructor behaviour on an empty chain, live token behaviour and signature replay. Nothing was left unreached. The scratch tests used for reproduction sit under the ignored `test/scratch/` directory and are not part of the submission.","treeHash":null,"usage":{"cachedInputTokens":1701372,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":29767,"runtime":"claude","turns":35,"wallClockMs":492598}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"de319b702da6aa2e","findings":[{"citation":"resolved","description":"Execution-trace / parameter-divergence: matchIds are first-come (open accepts any unused bytes32 from any caller, line 102), and join takes only the matchId and pulls whatever token and stake are recorded (line 131: _deposit(game.token, msg.sender, game.stake)). The joiner has no argument to assert the terms they agreed to off-chain. If an attacker opens a server-assigned matchId before the intended player A does (front-running A's open), a player B whose join is submitted against that ID (e.g. sent as soon as matchmaking pairs them, alongside A's open) deposits the attacker's token and stake tier. With the usual max/large approval this can be up to 50x the intended stake (100,000 vs 5,000,000 SHIFT), placed against a counterparty B never agreed to play. B gets the funds back only through a correct server result or the 24-hour refund. The author's docs/security-review.md acknowledges ID reservation races. The interface is fixed by the brief, so a fix needs a scope decision: the cheapest one inside the brief is client/server enforcement (B's client reads matches(matchId) and checks playerA/token/stake before joining, and the server refuses to sign for mismatched on-chain players). An on-chain fix would mean an extra join overload with expected token/stake, or binding matchId to the opener. Neither is in the specified ABI.","line":121,"path":"src/PinballEscrow.sol","reproduction":"Setup: SHIFT is a plain ERC-20; alice, bob and mallory each hold 10,000,000e18 SHIFT and approve the escrow for type(uint256).max. Server assigns id = keccak256(\"server-assigned\") to alice (opener, 100,000 SHIFT tier) and bob (joiner). 1) mallory.open(id, SHIFT, 5_000_000e18) is mined before alice's open. 2) alice.open(id, SHIFT, 100_000e18) reverts MatchAlreadyExists. 3) bob.join(id) succeeds. Expected: bob deposits 100,000e18 SHIFT into a match against alice, or the join reverts because the terms differ. Actual: bob's balance falls by 5,000,000e18 SHIFT and he is player B against mallory (verified with a local Foundry test: forge test on the sequence passes the assertion that bob paid 5_000_000e18).","severity":"low","snippet":"    function join(bytes32 matchId) external nonReentrant {\n        Match storage game = _matches[matchId];\n        if (game.state != State.Open) revert InvalidState();","title":"join() cannot bind the token/stake the joiner expects; a squatted matchId makes player B deposit attacker-chosen terms"}],"hash":"4be1b93b8c24334d03c388666ec6abcfa2b7ede74e2f80bebafcffaf96a9a01c","nodeId":"7f9ced3e-674b-473f-9fa5-c339e0787a94","outcome":"completed","summary":"I found one low-severity issue and nothing more serious. The results are in `.imd-findings.json`: one finding, plus coverage rows for all 7 entry points, the constructor and the balance invariant. No repository files were changed. I deleted my scratch test after running it.\n\n**Finding 1 (low): `join` can't pin the token or stake** (`src/PinballEscrow.sol:121`). Anyone can claim an unused match ID first. `join` takes only the ID and charges whatever token and stake that match recorded. So if an attacker opens a server-assigned ID before the real player A does, player B joins the attacker's match instead. If B has a large approval, he deposits the attacker's stake, up to 50 times what he expected (5,000,000 SHIFT instead of 100,000).\n- **Reproduction:** Mallory calls `open(id, SHIFT, 5_000_000e18)`, then Alice's `open` reverts with `MatchAlreadyExists`, and Bob's `join(id)` takes 5,000,000e18 SHIFT. I confirmed this with a local Foundry test.\n- **Fix:** the brief fixes the `join(bytes32)` signature, so an on-chain fix (an overload that states the expected terms, or tying the ID to the opener) needs the requester's decision. The cheaper fix within the brief: B's client reads `matches(id)` before joining, and the server refuses to sign results for players it didn't pair.\n- The author's own `docs/security-review.md` already lists this ID race as accepted. The finding adds the concrete stake exposure.\n\n**What holds** (traced through the Execution Trace, Periphery and First Principles guides):\n- **`settle`:**\n  - The typed-data name and the domain (\"SpacePinball\", \"1\", chain ID, this contract) match the brief exactly.\n  - The winner must be one of the two players, and a draw requires winner zero.\n  - Replays fail because a settled match can never be settled again.\n  - OpenZeppelin's signature recovery rejects malleable and wrong-length signatures. The contract-wallet fallback can't change state.\n  - The fee is exactly 5% on every allowed stake, with no rounding.\n- **Time windows:** the join and settle cutoffs and the two refund start times line up exactly, with no overlap and no gap at the boundary.\n- **Deposits:** they pull only from the caller and require the exact amount to arrive.\n- **Owner powers:** limited to `setSigner`, `pause` and `unpause` against the fixed owner address; while paused, only `open` stops.\n- **Funds:** each match pays out exactly what it took in, once, and stray token transfers to the contract are never counted.\n- **Constructor:** it requires the initial signer to equal the fixed owner address and calls no other contract.\n\n**Leads I dropped:**\n- Slither's \"unused return\" warning on `tryRecover`: the ignored value is only extra error detail.\n- The timestamp warnings: these are the brief's hour-long windows.\n- The \"centralization\" warning: these are the owner powers the brief asks for.\n\nThe existing suite passes (56 of 56 tests). Off-chain behaviour (the server's replay and signing, and front-end checks) was outside what I could verify.\n\nOne thing in the working tree: untracked entries like `.bashrc`, `.mcp.json` and `.claude/*` showed up during the run. They are empty device files placed by the sandbox, not files I created, so I left them alone.","treeHash":null,"usage":{"cachedInputTokens":575836,"inputTokens":20,"model":"claude-opus-5-5","outputTokens":15417,"runtime":"claude","turns":11,"wallClockMs":209848}},{"artifacts":[],"attempt":1,"bundleHash":"fc9b8761ed0051b9cffd61b01d6b835f6c18831e147ce624668f757c0619b5b8","device":"af1843f371022d4a","findings":[],"hash":"6f50f51236405f70b2512eb153c13dbfce93966ca8ba3655a652f7cf518d53a7","nodeId":"b073ecbb-6fff-419f-8974-2407e614445b","outcome":"completed","summary":"I've updated my tests for the revised `PinballEscrow`, and they found no defects. `forge build` succeeds and `forge test` passes 73 of 73 with the verifier's fuzz seed; my two suites also pass under a second seed.\n\nThe builder's revision added three things: `openInvite` (only a named invitee may join), `joinChecked` (join only if the token, stake and player A match), and an owner set by constructor argument. The builder's own unit tests already cover these, but my random-sequence invariant campaign didn't exercise them. Only files under `test/` changed:\n\n- **`test/helpers/ContinuousEscrowHandler.sol`**: two new random actions.\n  - `progressInvite` runs matches opened with `openInvite` and joined with `joinChecked`.\n  - `intrude` tries every way an ineligible join could happen on a live match: player A joining their own match, a non-invitee joining an invite-only match, and the right joiner calling `joinChecked` with the wrong token, tier or opponent. Each attempt must revert with its specific error and leave the match and all balances unchanged.\n  - Finished match IDs are now also retried with `openInvite` and `joinChecked`.\n  - After every call, the invariants also check that the recorded invitee is correct and that player B of an invite-only match is always the invitee.\n- **`test/PinballEscrowContinuousInvariant.t.sol`**: registers the two new actions (256 runs × 128 calls, no unexpected reverts). It also adds a fixed-sequence test showing each new path is actually reached, including a failed invitee deposit, paused invite-only opens and the refund of an invite that was never joined.\n- **`test/PinballEscrowEdges.t.sol`**: a new fuzz test, `testFuzz_ArbitraryInviteeIsTheOnlyPossibleOpponent`. For any invitee address, outsiders cannot join and lose nothing, and a signed result naming an outsider is rejected. When the invitee wins they get exactly 95% of the pot and the splitter 5%, and the escrow ends empty. Its first run failed because of my test, not the contract: the fuzzer picked the fee splitter as the outsider, which polluted the fee check. I fixed the test by generating outsider addresses and swapping reserved addresses out of the invitee.\n- **`test/CONTRIBUTOR-TESTS.md`**: describes the added coverage.\n\nI didn't write `.imd-findings.json` because no defect needs reporting. One point to check at launch: the contract no longer hard-codes the owner `0x6c4c…19b5` from the brief, and accepts any non-zero owner. That follows the launch guidance, but the deployment manifest must pass that address as the owner, and as the initial signer if the signer is meant to equal the owner.","treeHash":"ec8cc16b8ec002e60310057efd2fe170d0c56881","usage":{"cachedInputTokens":784477,"inputTokens":26,"model":"claude-opus-5-5","outputTokens":15655,"runtime":"claude","turns":14,"wallClockMs":337400}},{"artifacts":[],"attempt":1,"bundleHash":"8874c165ec545fc4fa91b1866ce56dc86eded4387ff58340c8715e6692c534c2","device":"0ef0cddb99617930","findings":[],"hash":"836710b9b89d0424e7c8b68a6db933f4c0fb0a61cca155f085e4393e5cd7613f","nodeId":"be66ad88-73e6-4554-8ac9-1b2e294ae4bc","outcome":"completed","summary":"The finding held and launch.json is fixed. The response file is written and only the manifest changed.\n\n**What the finding showed.** The accepted source now has a two-argument constructor, `constructor(address owner_, address initialSigner)`, added when the earlier owner finding was fixed. The manifest still passed a single `$owner` entry, so it did not match the constructor at all. Encoding it would have reverted the factory's CREATE2 deployment.\n\n**What I changed.** The manifest now lists two constructor arguments, both the static address 0x6c4c78f2c3bdf4c4318fe305fb1c8bc1694d19b5. The brief states that owner address, so the rules require the static word rather than `$owner`. The brief also requires the initial signer to equal the owner, so the second argument is the same static word. The notes were updated to describe the two-argument constructor.\n\n**Verification.**\n- A scratch test deployed PinballEscrow via CREATE2 from a factory contract with a foreign tx.origin on chain id 1, using exactly the manifest's arguments. It confirmed owner and signer equal the brief's address and that neither is the factory or the launch wallet.\n- Manifest shape validated: only kind, contracts and notes, string arguments, identifier within 32 characters, notes under 4000 characters.\n- `forge build` and `forge test` with the verifier's fuzz seed: all 65 project tests pass.\n\nThe earlier owner finding was already fixed by the author and is settled by the same scratch test, which shows owner follows the constructor argument, not msg.sender or tx.origin. The response for the reopened finding is recorded in `.imd-responses.json` with verdict fixed.","treeHash":"f325bf1ce45c1fe044363f86ebab3f99e8e46829","usage":{"cachedInputTokens":431434,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":6965,"runtime":"claude","turns":11,"wallClockMs":141648}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a18a0c6087e1362f","findings":[{"citation":"resolved","description":"Flow-gap seam (execution x first principles). The brief's purpose is 'two players stake the same amount of the same token' for a match the NIGHTSHIFT server arranged. open() reserves any bytes32 ID first-come, and join() takes only the ID: it reads token, stake and playerA from storage and pulls game.stake of game.token from msg.sender (line 131) with no way for B to state what they expected. Because open() is permissionless and IDs are visible off-chain (the server hands the same ID to both players, and A's open() sits in the public mempool), an outsider can (a) front-run A's open(ID, IMD, 1e18) with open(ID, SHIFT, 5_000_000e18), or (b) front-run B's join on A's real match. In case (a) B's join(ID) debits 5,000,000 SHIFT instead of 1 IMD and records Mallory, not Alice, as the opponent; the tx shows no amount to B's wallet because the transfer is internal, and any standing approval covers it. In case (b) Alice's stake is paired with a stranger. Economic consequences, all reachable by an unprivileged actor: (1) the server's result for the pairing it arranged (winner = Alice) can never settle this pot because line 145 requires winner to be a recorded player, so B's 5,000,000 SHIFT (or A's stake) is locked until joinedAt + 24h and then refunded without compensation, at an attacker cost of gas plus their own refundable stake; (2) if the server instead signs for the on-chain pair, B has been forced into a 5,000,000 SHIFT game they consented to only as a 1 IMD game, and a stronger attacker (bot) extracts 9,500,000 SHIFT from a casual player who approved more than one stake; (3) any ID the server assigns predictably can be burnt forever for one minimum stake locked one hour, since terminal IDs are tombstones (line 102). docs/security-review.md acknowledges open/join can be front-run and defers detection to clients and the server; the contract itself gives B no on-chain guard. Existing tests never exercise a second-party open on an ID B then joins. Proposed minimal fix preserving the design: have join take the values B agreed to (expected token, stake and playerA) and revert on mismatch, or equivalently let open record an invited player B; both change the join(bytes32) signature fixed by the brief, so this is a scope decision for the requester. A mitigation without an interface change is for the server to sign only after verifying the on-chain pair and stake, and for clients to re-read matches(id) in the same block before sending join, which narrows but does not close the mempool race.","line":121,"path":"src/PinballEscrow.sol","reproduction":"State: escrow deployed with signer set; IMD and SHIFT are plain ERC-20s; Alice, Bob and Mallory each hold 100 IMD and 10,000,000 SHIFT and have approved the escrow for type(uint256).max (or at least 5,000,000e18 SHIFT). ID = keccak256('server-assigned match 42') is the ID the server gave Alice and Bob for a 1 IMD game. Calls: 1) Mallory: open(ID, SHIFT, 5_000_000e18) (front-run). 2) Alice: open(ID, IMD, 1e18) reverts MatchAlreadyExists. 3) Bob: join(ID) succeeds. Expected per brief: Bob stakes 1 IMD against Alice. Actual: Bob's IMD balance is unchanged and his SHIFT balance drops by exactly 5_000_000e18; matches(ID) = {token: SHIFT, stake: 5_000_000e18, playerA: Mallory, playerB: Bob, state: Joined}. 4) Relayer: settle(ID, Alice, false, now+1h, validSignerSignature) reverts InvalidWinner (Alice is not a participant). 5) refund(ID) reverts RefundNotReady until block.timestamp == joinedAt + 86400, then returns 5,000,000 SHIFT to Bob and Mallory. Variant: 1) Alice: open(ID, IMD, 25e18). 2) Mallory: join(ID) (front-run). 3) Bob: join(ID) reverts InvalidState; matches(ID).playerB == Mallory; settle(ID, Bob, ...) with a valid signature reverts InvalidWinner; Alice's 25 IMD is locked until joinedAt + 24h. Verified with a scratch Foundry test (two tests, both pass on the current code, demonstrating the behaviour above).","severity":"medium","snippet":"    function join(bytes32 matchId) external nonReentrant {\n        Match storage game = _matches[matchId];\n        if (game.state != State.Open) revert InvalidState();\n        if (msg.sender == game.playerA) revert CannotJoinOwnMatch();\n        if (block.timestamp >= game.openedAt + JOIN_WINDOW) revert JoinWindowClosed();","title":"join(bytes32) commits player B to whatever token, stake and opponent the ID currently holds, so a front-run open/join silently changes B's deposit and locks it for 24 hours"}],"hash":"8ef8929d2ef9e98ed9898cb363f66caedf958040b800a1c5c49bf98ee49c0e6a","nodeId":"95601a56-9531-4fb6-95e6-5a5490a9220d","outcome":"completed","summary":"Review complete. The findings file is written and valid, the scratch test directory is removed, and the working tree has no new tracked files.\n\n**Outcome:** one substantiated finding, medium severity, in `.imd-findings.json`, plus coverage rows for all seven entry points and four economic invariants.\n\n**The finding.** The `join` entry point takes only a match ID and pulls whatever token, stake and opponent that ID currently holds from storage. Since `open` is permissionless and first-come, an outsider who sees the server-assigned ID can front-run Alice's open with a different token and tier, or front-run Bob's join on Alice's real match. Reproduced in a scratch Foundry test on the current code:\n\n- Mallory opens the ID with 5,000,000 SHIFT before Alice's 1 IMD open lands. Bob's `join(ID)` then debits 5,000,000 SHIFT, not 1 IMD, and pairs him with Mallory.\n- The server's result for the pairing it arranged reverts with `InvalidWinner`, so the stake is locked until the 24-hour refund. If the server signs for the on-chain pair instead, Bob is forced into a game at a stake he never consented to.\n- The author's own security review acknowledges this race and defers it to clients and the server. The brief fixes the `join(bytes32)` signature, so the fix is a scope decision: either let B pass the expected token, stake and player A, or have A name an invited opponent.\n\n**What holds in my area.** Settlement pays exactly 95% to a recorded player and exactly 5% to the splitter with no rounding dust on any of the eight tiers. Draws and both refund paths return exact stakes. The join/refund and settle/refund windows are disjoint at the boundaries. Match IDs are single-use tombstones, so signatures cannot replay. The contract balance always equals outstanding stakes plus donations, confirmed by the existing stateful invariant suite, which I ran along with all 56 tests. The owner's powers are limited to signer rotation and pausing opens, and pausing traps no funds.\n\n**Not reported as findings.** The missing signer epoch in the signed Result struct is documented in the README and requires an owner action to exploit, so it appears only as a coverage note. Joins remain allowed while paused, which the brief leaves open and the author documents as intentional.","treeHash":null,"usage":{"cachedInputTokens":999678,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":24456,"runtime":"claude","turns":27,"wallClockMs":390115}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"45aa937328087de3","findings":[{"citation":"resolved","description":"Prior id 93ef1f4d19fc587e8492c7cb82cd3088ae0eb57e7400cfef514424cfe5bc6d2d, answered [disputed] as a documented trust assumption. Re-traced this round: settle() still compares the recovered address only against the current `signer` and the brief-fixed Result struct (matchId, winner, draw, deadline) carries no signer identity or epoch, so setSigner(K2) followed by setSigner(K1) revives K1's unexpired signatures for still-Joined matches. The author's answer is accepted: the brief fixes the Result type, only the owner can trigger the revival, and the README ('rotation suspends, it does not revoke; never reinstate a key that may have signed a disputed result') and docs/security-review.md now state the operational rule with the finding id. This row records the trust assumption so the owner's key-rotation procedure is explicit; it does not reopen the work.","line":160,"path":"src/PinballEscrow.sol","reproduction":"test/scratch/Settle.t.sol::test_rotationRevives (passes on this code, i.e. the behaviour still occurs): signer = K1; alice open(id, IMD, 1e18), bob join(id); K1 signs Result(id, bob, false, now + 20h) -> sigK1; owner setSigner(K2); settle(id, bob, false, dl, sigK1) reverts InvalidSignature; owner setSigner(K1); settle(id, bob, false, dl, sigK1) succeeds and bob receives 1.9e18 IMD. Expected if rotation were revocation: the last call reverts. Actual: it settles. Accepted as a documented owner trust assumption under the brief's fixed Result type.","severity":"info","snippet":"            (error != ECDSA.RecoverError.NoError || recovered != signer)","title":"Settled, no revision needed: signer rotation suspends rather than revokes a retired key's unexpired results; documented as an owner operational rule"}],"hash":"a9a54e2b816d9da0ed41cae4ffb3ea6e2c4c1b973638f0b3576860b61d21809a","nodeId":"b86952db-202a-4d49-a1c5-54f6432251de","outcome":"completed","summary":"Review complete. Both prior blocking findings are fixed, nothing new reproduces, and the report is in `.imd-findings.json` with a clean working tree.\n\n**Settled findings**\n\n- **Match ID squat and slot snipe (medium): fixed.** The revised contract adds `joinChecked`, which reverts with MatchMismatch unless the match holds exactly the agreed token, stake and opener, and `openInvite`, which records an invitee so any other joiner reverts with NotInvited. I re-ran both of my earlier scenarios against the new entry points. Bob's checked join against a squatted ID reverts with his balances untouched, and a stranger cannot take Bob's slot on an invited match. The original `open` and `join` signatures from the brief are unchanged.\n- **Manifest passed $owner to a constructor that required the fixed owner (medium): fixed.** The manifest now carries two static owner words matching the two-argument constructor. I ran the protected deployment rehearsal with the compiled creation code and those arguments through its CREATE2 probe. It deploys at the predicted address, stays under the size limits, and contains no forbidden opcodes.\n- **Signer rotation revives a reinstated key's old signatures (info): accepted as documented.** It still reproduces, but the brief fixes the Result struct, only the owner can trigger it, and the README and security review now state the operational rule. Recorded as info so it does not reopen the work.\n\n**Fresh pass**\n\nThe full project suite passes, 73 tests including the invariant suites with the pinned fuzz seed. All nine ABI entry points are marked `holds` in the coverage record. The slither unused-return lead is the harmless third return of tryRecover and was dropped. The one residual, that a plain `join` still binds the joiner to whatever the ID holds, is inherent to the brief's fixed signature and is documented as a client rule rather than reported as a defect.","treeHash":null,"usage":{"cachedInputTokens":713836,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":13988,"runtime":"claude","turns":18,"wallClockMs":240459}},{"artifacts":[],"attempt":1,"bundleHash":"b54951c1b758be1b32b063b1c1e8fc7d6be85ecfa9dc9fcf79f2249fa63dab7b","device":"d0cae3c2b90f3949","findings":[],"hash":"b29525166dcdeb1396f98d8b41e2dcdc0bd4a8ae64c908a1a33d9ccffafc4828","nodeId":"45aa6ab3-8c75-45cd-99dd-5c5b535da8f8","outcome":"completed","summary":"Implemented [PinballEscrow](/root/.identitymd/work/fd2b803a-4477-4ac6-9d75-a5c1c16df44e/45aa6ab3-8c75-45cd-99dd-5c5b535da8f8/src/PinballEscrow.sol), vendored dependencies, and deployment/operations documentation.\n\nVerification passed:\n\n- `forge build`\n- `forge test`: 56 tests using the required seed\n- `forge fmt --check`\n- Offline testing and 8,192 invariant calls\n- 100% escrow coverage\n\nReady for IdentityMD’s review and deployment stage. No mainnet transaction was broadcast.","treeHash":"84755c304b253ced47b3023a352b760d742582dc","usage":{"cachedInputTokens":1350784,"inputTokens":95607,"model":"gpt-6-astra","outputTokens":44881,"runtime":"codex","turns":8,"wallClockMs":982711}},{"artifacts":[],"attempt":1,"bundleHash":"b72a01ea4a29e9f24797b6c04cdb9b505f733e0746aa96d23f445bc9ecdc9ff0","device":"c0fc4ea4f50e3380","findings":[{"description":"PinballEscrow's constructor reverted when deployed as the launch deploys it, on a chain where no other contract exists: take outside values as arguments and check the live contract on first use\n\nRepair: this contract's owner()/admin()/getOwner()/DEFAULT_ADMIN_ROLE resolves to the launch factory (the msg.sender at deploy) or to IMD (tx.origin or the launch wallet). Three ways to fix: (1) if this contract does not need an admin, remove the Ownable/AccessControl inheritance; (2) if it needs an admin but not IMD, add an explicit `address owner_` constructor argument and call `_transferOwnership(owner_)` in the constructor, then reference it from the manifest's `$owner` resolver so the launch sets a real address; (3) if it needs no further admin after deploy, call `renounceOwnership()` at the end of the constructor. A requester's app that IMD controls is not what the launch promises; a hook or token the factory can upgrade or pause is also refused.","line":null,"path":"src/PinballEscrow.sol","reproduction":"The verifier deployed the manifest's contracts as the launch deploys them: from the launch factory (msg.sender), with IMD's launch wallet as tx.origin and $owner set, then read owner(), admin(), getOwner() and the admin role, and for a hook opened, seeded, traded and collected the pool from the factory (test/protected/evm_contracts/Owners.protected.t.sol in the protected floor).","severity":"high","title":"PinballEscrow is owned by the launch factory or by IMD"}],"hash":"b967ca41d0250c79f1618308f402c35dc5d78d7cb595187eebab7bf8588457f0","nodeId":"be66ad88-73e6-4554-8ac9-1b2e294ae4bc","outcome":"completed","summary":"The build passes, all 56 tests pass with the verifier's fuzz seed, and the manifest validates. Only launch.json is new in the working tree.\n\n**What I did**\n\n- Wrote `launch.json` for the single accepted contract, PinballEscrow, with one constructor argument filled by `$owner`.\n- The constructor's only parameter is the initial result signer. The contract reverts unless it equals the compiled-in owner address, so `$owner` is the only correct value.\n- Tokens, stake lists, the 5% fee and the fee splitter are constants in the source, not constructor inputs, so there are no other arguments.\n- The notes field describes the owner's limited powers, the fixed constants and the EIP-712 domain, and stays well under the 4000-character limit.\n\n**Checks**\n\n| Check | Result |\n|---|---|\n| forge build | ok |\n| forge test with FOUNDRY_FUZZ_SEED set | 56 passed, 0 failed |\n| Manifest keys and notes length | kind, contracts, notes only |\n| Files changed | launch.json only |\n\nThe protected deployment probe will run the constructor on an empty chain. The constructor makes no external calls, so that rehearsal is expected to pass. The owner check also passes since ownership is a constant set to the brief's owner, not the factory.","treeHash":"3dcb4ddd9bf183075a255fd132f354a79c40fc12","usage":{"cachedInputTokens":171453,"inputTokens":130,"model":"claude-fable-5-1","outputTokens":2355,"runtime":"claude","turns":7,"wallClockMs":43490}},{"artifacts":[],"attempt":1,"bundleHash":"86d1d2932046f5ed5ab433a431003e3ed85f3aa3b23c733567bc3212a71ab81e","device":"ad944e1953b3faa2","findings":[],"hash":"c1516214714a1bc10fcdee292856a347228a0872a9eec3b6c8a81496725a8299","nodeId":"b073ecbb-6fff-419f-8974-2407e614445b","outcome":"completed","summary":"Added tests under `test/` for stake boundaries, exact deposits, failed-payment rollback and retries, plus continuous stateful invariants.\n\nVerified offline with the required fuzz seed:\n\n- `forge build`: passed\n- `forge test`: 63 passed, 0 failed\n- 40,960 randomized invariant calls passed\n\nNo confirmed implementation defects found.","treeHash":"0a860855ae4cdf91b7b6775ea904e942dd4601db","usage":{"cachedInputTokens":1117056,"inputTokens":83145,"model":"gpt-6-astra","outputTokens":20013,"runtime":"codex","turns":7,"wallClockMs":493360}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9e51ef2afd7c2af8","findings":[{"citation":"resolved","description":"join() accepts the first caller that is not playerA and has the stake; open() accepts any bytes32 ID from any caller (line 102: `if (_matches[matchId].state != State.None) revert MatchAlreadyExists();`). The brief's matchmaking (two specific players, the NIGHTSHIFT server replays both of their games) therefore has no on-chain binding. Two concrete griefs follow. (1) Slot sniping: Charlie watches the mempool for Alice's open and joins before Bob. Bob's join reverts with InvalidState. If Charlie never plays, the server has nothing to sign; Alice's stake stays locked until joinedAt + 24h, when refund() returns it. Charlie pays only gas and the same temporary lock on his own stake. (2) ID squatting: if the server assigns predictable IDs, Charlie opens the ID first with the cheapest tier (1 IMD); Alice's open for the same ID reverts with MatchAlreadyExists, and Charlie refunds himself after one hour. Neither grief loses funds, so this is low. The author lists this as an accepted interface limitation in docs/security-review.md ('Public open/join calls allow competing transactions to reserve an ID or take the second slot'). It is recorded here so the judge can decide whether an intended-opponent field or server-signed join authorisation is wanted; adding one changes the agreed interface. Fee math, window arithmetic and signature boundaries, the assigned area, hold (see coverage).","line":127,"path":"src/PinballEscrow.sol","reproduction":"State: fresh deployment, IMD and SHIFT plain ERC-20 at the fixed addresses, alice/bob/charlie funded and approved. Sequence A: vm.prank(charlie); escrow.open(keccak256('server-assigned'), IMD, 1e18); vm.prank(alice); escrow.open(keccak256('server-assigned'), SHIFT, 100_000e18) -> reverts MatchAlreadyExists (expected per brief: Alice's server-assigned match opens). Sequence B: vm.prank(alice); escrow.open(id2, SHIFT, 5_000_000e18); vm.prank(charlie); escrow.join(id2) succeeds; vm.prank(bob); escrow.join(id2) -> reverts InvalidState; escrow.matches(id2).playerB == charlie. settle() for id2 now requires winner in {alice, charlie}; refund(id2) only succeeds once block.timestamp >= joinedAt + 86400, so 5,000,000 SHIFT of Alice's stays escrowed for 24h. Reproduced in test/scratch/MathBoundary.t.sol::test_idSquatAndJoinFrontRun (passes, i.e. the behaviour occurs).","severity":"low","snippet":"        game.playerB = msg.sender;\n        game.joinedAt = block.timestamp;\n        game.state = State.Joined;","title":"Open matches have no bound opponent: anyone can take the player-B slot (or squat a match ID) and lock player A's stake for 24 hours at no cost beyond gas"}],"hash":"c334d7227b758c9612ed9cbb93f4f816fc02b9c86805c493d111bdbc983d1f69","nodeId":"ab864108-36b8-4559-abb9-d2a1162b7662","outcome":"completed","summary":"The review is complete and the findings file is written and validated.\n\n**Outcome:** the math, boundary and numerical-gap area of PinballEscrow holds. I found no defect in it. One low-severity griefing lead outside the area is recorded with a reproduction.\n\n**What I verified** (all confirmed with a scratch Foundry test under test/scratch/, which passed):\n- **Fee precision.** For all eight fixed tiers the pot is a multiple of 20 wei-scaled tokens, so the 5% fee is an exact integer, winner payout plus fee equals the pot, and the escrow holds zero residue after settlement.\n- **Overflow.** The largest intermediate is about 5e27, far below the uint256 limit. No unchecked blocks, downcasts or variable divisors exist.\n- **Window edges.** Join stops at exactly one hour and refund starts at that same second. Settle stops at exactly 24 hours and refund starts there. The signature deadline is inclusive. No timestamp lets a payout path and a refund path both succeed.\n- **Boundary inputs.** Empty, 64-byte and high-s signatures, an outsider winner with a valid signature, a draw with a non-zero winner, and a zero winner without draw all revert as intended. Double settle reverts.\n- **External calls.** Deposits are checked by exact balance delta, SafeERC20 rejects no-code and false-returning tokens, and every transfer happens after the state write under the reentrancy guard.\n\n**The one finding (low):** open matches bind no opponent. Anyone can take the player-B slot before the intended opponent, and if they never play, player A's stake stays locked for 24 hours. A predictable match ID can also be squatted with the cheapest tier. No funds are lost, and the author already lists this as an accepted interface limitation in docs/security-review.md. It is reported so the judge can decide whether an intended-opponent binding is wanted.\n\n**Coverage record:** all seven entry points have a row. Six hold, and join carries the finding. Four invariant rows record the precision, overflow, external-call and window-boundary checks.\n\n**Not performed:** Slither and Mythril were not run beyond the supplied static output, whose unused-return and timestamp leads I checked and dismissed as non-defects. The live token contracts were not examined; the review assumes plain 18-decimal ERC-20 behaviour as the brief states.","treeHash":null,"usage":{"cachedInputTokens":828192,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":23087,"runtime":"claude","turns":26,"wallClockMs":344538}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1ec9f666356a7ad7","findings":[{"citation":"resolved","description":"Merged from audit_permissions (low), audit_math (low), audit_economics (medium) and audit_flow (low); all four describe the same root cause and all four reproduce. open() reserves any unused bytes32 for whoever lands first (line 102: `if (_matches[matchId].state != State.None) revert MatchAlreadyExists();`) and records the opener's choice of token and stake. join() takes only the ID and pulls game.stake of game.token from msg.sender (line 131) with no argument through which B can assert the token, stake or opponent agreed off-chain. Because the NIGHTSHIFT server hands the same ID to both players and A's open() sits in the public mempool, an unprivileged third party can (a) front-run A's open with the same ID and a different tier, so A's open reverts and B's join, covered by a standing allowance, deposits up to 5,000,000 SHIFT instead of 1 IMD into a match against the squatter; or (b) front-run B's join on A's real match, so B's join reverts and A's stake is paired with a stranger. In either case the server's result for the arranged pairing can never settle (line 145 requires the winner to be a recorded player), so the affected stake is locked until joinedAt + 24 hours and then refunded by refund(); any predictably assigned ID can also be burnt forever for one minimum stake locked one hour, because terminal IDs are tombstones. No funds are lost and the attacker's own stake is locked for the same period, so this is a griefing and consent defect, not theft; it is medium because any mempool observer can repeatedly break the brief's core guarantee that the two matched players stake into their own match, and the contract gives B no on-chain guard. The author documents it in docs/security-review.md as an accepted interface limitation. Fix options, which need a scope decision because the brief fixes open(bytes32,address,uint256) and join(bytes32): (1) keep both signatures but require in open() that the match ID carries the opener, e.g. `if (address(bytes20(matchId)) != msg.sender) revert ...;` so only A can reserve her own IDs and B's client can read the opener from the ID before joining (closes squat (a), not slot-snipe (b)); (2) add a join overload join(bytes32,address token,uint256 stake,address playerA) that reverts on mismatch; (3) record an invited player B in open(). If the interface must stay, the server must verify matches(id) on-chain before signing and clients must approve only the exact stake per call, which is an off-chain mitigation, not enforcement.","line":121,"path":"src/PinballEscrow.sol","reproduction":"State: fresh deployment with signer = owner; IMD and SHIFT plain ERC-20 at the fixed addresses; alice, bob and mallory each hold 100 IMD and 10,000,000 SHIFT and have approved the escrow for type(uint256).max on both tokens. The server gave alice and bob id = keccak256('server-assigned match 42') for a 1 IMD game. Calls: (1) mallory: open(id, SHIFT, 5_000_000e18) mined first. (2) alice: open(id, IMD, 1e18) reverts MatchAlreadyExists. (3) bob: join(id) succeeds. Expected per brief: bob stakes 1 IMD against alice, or the join reverts. Actual: bob's IMD balance is unchanged and his SHIFT balance drops by exactly 5_000_000e18; matches(id) = {token: SHIFT, stake: 5_000_000e18, playerA: mallory, playerB: bob, state: Joined}. (4) refund(id) reverts RefundNotReady until block.timestamp >= joinedAt + 86400, then returns 5,000,000 SHIFT to bob and mallory. Variant: alice: open(id2, IMD, 25e18); mallory: join(id2) front-runs; bob: join(id2) reverts InvalidState; matches(id2).playerB == mallory; settle(id2, bob, false, ...) with a valid signer signature reverts InvalidWinner; alice's 25 IMD is locked until joinedAt + 24h. Reproduced in test/scratch/Repro.t.sol (test_squatChangesBobDeposit and test_slotSnipe, both pass on the current code, i.e. the behaviour occurs).","severity":"medium","snippet":"    function join(bytes32 matchId) external nonReentrant {\n        Match storage game = _matches[matchId];\n        if (game.state != State.Open) revert InvalidState();\n        if (msg.sender == game.playerA) revert CannotJoinOwnMatch();\n        if (block.timestamp >= game.openedAt + JOIN_WINDOW) revert JoinWindowClosed();\n\n        game.playerB = msg.sender;\n        game.joinedAt = block.timestamp;\n        game.state = State.Joined;\n        emit Joined(matchId, msg.sender);\n        _deposit(game.token, msg.sender, game.stake);","title":"Match IDs are first-come and join(bytes32) binds player B to whatever the ID holds: a front-run open or join changes B's token/stake tier or A's opponent and locks the stake for 24 hours"},{"citation":"resolved","description":"PinballEscrow has no owner argument: `owner` is the constant 0x6C4c78f2C3bDf4C4318Fe305FB1C8BC1694d19b5 (src/PinballEscrow.sol line 18) and the constructor enforces `if (initialSigner != owner) revert InvalidInitialSigner();` (line 81). The manifest fills the only constructor argument with `$owner`. Under the launch rules, `$owner` resolves to the wallet that paid for the launch or to the owner the request's owner field names, and an owner address the brief itself states is to be written as that static address instead of `$owner`. The brief states the owner address, so the manifest form prescribed is the static word 0x6c4c78f2c3bdf4c4318fe305fb1c8bc1694d19b5. If the resolver's `$owner` is any other address (the paying wallet, or an owner field that differs from the brief's text), the factory's CREATE2 of PinballEscrow reverts in the constructor and the whole launch is parked, with nothing that can be fixed after the fact. Writing the static address is correct under either resolution and removes the dependency. The README (line 28) already assumes `$owner` equals the fixed owner, which is the assumption this finding asks to be made explicit in the manifest.","line":7,"path":"launch.json","reproduction":"Code: `new PinballEscrow(X)` for any X != 0x6C4c78f2C3bDf4C4318Fe305FB1C8BC1694d19b5 reverts with InvalidInitialSigner (test/scratch/Repro.t.sol::test_constructorRejectsNonOwnerSigner passes with X = makeAddr('payer'); the project's own test_ConstructorRejectsIncorrectOrZeroInitialSigner shows the same). Manifest: launch.json contracts[0].constructorArgs = [\"$owner\"]. Expected: the manifest argument is guaranteed to equal the constant the constructor requires, i.e. the static address \"0x6c4c78f2c3bdf4c4318fe305fb1c8bc1694d19b5\". Actual: it is \"$owner\", whose value is decided by the launch's paying wallet or request owner field; if that resolves to anything but 0x6c4c…19b5 the factory deployment reverts (the protected ContractsProtectedTest would fail with 'application 0 constructor failed') and the launch cannot deploy.","severity":"medium","snippet":"        \"$owner\"","title":"launch.json passes $owner as initialSigner, but the constructor reverts unless the argument equals the hard-coded owner 0x6C4c…19b5; a brief-stated owner address must be written as that static address"},{"citation":"resolved","description":"From audit_permissions; reproduced by tracing. settle() accepts any signature from the current `signer` over Result(matchId, winner, draw, deadline) (lines 149-153); the signed struct carries no signer identity, nonce or epoch, and setSigner() only overwrites the single slot. Rotating away from a key makes its outstanding signatures fail (project test test_SignerRotationInvalidatesOutstandingOldKeyResults), but rotating back to it re-enables every signature it produced whose deadline has not passed and whose match is still Joined inside its 24-hour window. Only the owner can trigger this, and the author documents it in docs/security-review.md, so it is a trust assumption for the owner's key-rotation procedure, not a defect requiring a revision: never reinstate a key that may have signed a disputed result. If the brief may change, adding the signer address or an epoch counter to the signed struct makes rotation a revocation.","line":190,"path":"src/PinballEscrow.sol","reproduction":"State: signer = K1; match ID joined at t0 between alice and bob. (1) K1 signs Result(ID, bob, false, t0 + 20 hours) -> sigK1. (2) owner: setSigner(K2). (3) settle(ID, bob, false, t0 + 20h, sigK1) reverts InvalidSignature. (4) owner: setSigner(K1) at t0 + 1 hour. (5) settle(ID, bob, false, t0 + 20h, sigK1) succeeds and bob receives 95% of the pot. Expected if rotation were revocation: step 5 reverts. Actual: it settles with the pre-rotation signature, because line 151 compares the recovered address only against the current `signer`.","severity":"info","snippet":"        signer = newSigner;","title":"Signer rotation suspends rather than revokes a key's outstanding results: re-selecting a retired signer revives its unexpired signatures for still-joined matches"}],"hash":"f58deab04cddf4022c4f617d052c750725a9c184d34e65f18808ee33995defc6","nodeId":"b86952db-202a-4d49-a1c5-54f6432251de","outcome":"completed","summary":"The findings file is valid JSON, the quoted snippets match the tree at the cited lines, and the only files I created are the findings file and the scratch test, which is not kept.\n\n**Review outcome:** the escrow's core logic holds. All 63 project tests pass. Settlement, refund windows, fee math, signature binding, pause scope and owner powers all behave as the brief specifies. Three findings were written to `.imd-findings.json`, with all seven entry points covered.\n\n- **Medium, merged front-run finding.** All four specialists reported the same root cause, and I merged them into one. Any caller can reserve any unused match ID, and join takes only the ID, so a mempool observer can front-run A's open with a larger tier or front-run B's join. B then deposits up to 5,000,000 SHIFT against a stranger, or A's stake is paired with a stranger, and the arranged result can never settle. Funds are locked for 24 hours and then refunded, so this is griefing, not theft. I reproduced both variants in a scratch test. The fix needs a scope decision since the brief fixes both signatures. I listed three options, the cheapest being an opener-encoded ID check in open.\n- **Medium, manifest argument.** The constructor reverts unless the initial signer equals the hard-coded owner. The manifest passes `$owner`, which the launch rules resolve to the paying wallet or the request's owner field. The rules say a brief-stated owner is written as its static address. If `$owner` resolves to anything else, the factory deployment reverts and the launch cannot deploy. Writing the static address is correct under either resolution.\n- **Info, signer epoch.** Rotating back to a retired signer revives its unexpired results. Owner-only and documented by the author, so recorded as a trust assumption only.\n\n**Dropped or settled leads:** the slither unused-return line is a false positive, since both the recovered address and the error are consumed. The timestamp and centralization lines implement requested behaviour. Nothing further surfaced in my own pass over the code.","treeHash":null,"usage":{"cachedInputTokens":472734,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":17321,"runtime":"claude","turns":14,"wallClockMs":264348}}],"verification":[{"checks":[{"durationMs":2258,"exitCode":0,"name":"build","output":"Compiling 54 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.18s\nCompiler run successful!\n","passed":true},{"durationMs":4334,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 15 tests for test/PinballEscrowTokens.t.sol:PinballEscrowTokensTest\n[PASS] test_DrawSecondTransferFailureAlsoRollsBackFirstRefund() (gas: 596782)\n[PASS] test_FalseReturnAndRevertingDepositsCannotCreateOrJoinMatches() (gas: 915655)\n[PASS] test_FeeTransferFailureAlsoRollsBackWinnerPayment() (gas: 579052)\n[PASS] test_InexactDepositsRollbackStateBalancesAndAllowances() (gas: 1287874)\n[PASS] test_InsufficientBalanceDoesNotReserveAnIdOrPlayerSlot() (gas: 700297)\n[PASS] test_MissingAllowanceRollsBackOpenAndJoin() (gas: 824062)\n[PASS] test_MissingTokenCodeFailsOnUseAndLeavesMatchUncreated() (gas: 186812)\n[PASS] test_ReentrancyCannotMoveAnotherMatchsDeposits() (gas: 961478)\n[PASS] test_ReentrancyDuringOpenAndJoinIsRejectedWithEffectsAlreadyApplied() (gas: 749268)\n[PASS] test_ReentrancyDuringRefundCannotRefundAgain() (gas: 680731)\n[PASS] test_ReentrancyDuringSettlementCannotSettleTwiceOrOpenAnotherMatch() (gas: 1451200)\n[PASS] test_RefundFailureIsAtomicAndDoesNotBlockUnrelatedMatches() (gas: 834433)\n[PASS] test_SafeERC20SupportsTokensWithoutReturnValues() (gas: 517556)\n[PASS] test_UnjoinedRefundFailureCanBeRetried() (gas: 405331)\n[PASS] test_WinnerTransferFailureIsAtomicAndRetryable() (gas: 609436)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 32.38ms (6.47ms CPU time)\n\nRan 16 tests for test/PinballEscrowSignatures.t.sol:PinballEscrowSignaturesTest\n[PASS] testFuzz_SignerCannotAwardPotToAnOutsider(address) (runs: 512, μ: 364512, ~: 364570)\n[PASS] test_CannotReplayAcrossContracts() (gas: 972399)\n[PASS] test_ChainIdChangeRejectsOldSignatureAndAcceptsNewDomain() (gas: 481648)\n[PASS] test_DomainAndResultDigestMatchIndependentEncoding() (gas: 31270)\n[PASS] test_DrawCannotBeSettledAgain() (gas: 445590)\n[PASS] test_DrawRequiresCanonicalNoWinner() (gas: 441864)\n[PASS] test_ECDSASignerWithCodeCanStillSignDirectly() (gas: 444118)\n[PASS] test_ERC1271SignerAcceptsValidSignatureAndRejectsRevocationAndReverts() (gas: 1054975)\n[PASS] test_ExpiredSignatureRejectedButExactDeadlineAccepted() (gas: 457378)\n[PASS] test_MalformedInvalidVAndHighSSignaturesRejected() (gas: 692830)\n[PASS] test_MatchIdWinnerDrawAndDeadlineAreAllBound() (gas: 1037821)\n[PASS] test_ReplayAndFreshConflictingResultCannotDoubleSettle() (gas: 490759)\n[PASS] test_SignerRotationInvalidatesOutstandingOldKeyResults() (gas: 502880)\n[PASS] test_UnavailableSignerCannotPreventTimedRefund() (gas: 797022)\n[PASS] test_WrongDomainNameVersionAndPersonalSignAreRejected() (gas: 481843)\n[PASS] test_WrongSignerIsRejected() (gas: 404797)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 36.21ms (45.94ms CPU time)\n\nRan 32 tests for test/PinballEscrow.t.sol:PinballEscrowTest\n[PASS] testFuzz_AllowedStakesConserveFunds(bool,uint8,uint8,bytes32) (runs: 512, μ: 538192, ~: 542760)\n[PASS] testFuzz_BothRefundPathsBothTokens(bool,bool,uint8) (runs: 512, μ: 523123, ~: 461241)\n[PASS] testFuzz_JoinCheckedBindsOnlyTheExactParameters(bool,uint8,address,uint256,address) (runs: 512, μ: 273907, ~: 273888)\n[PASS] testFuzz_OnlyOwnerCanAdminister(address) (runs: 512, μ: 76101, ~: 76156)\n[PASS] testFuzz_OwnerFollowsConstructorArgumentNotDeployerOrOrigin(address,address) (runs: 512, μ: 368372, ~: 368462)\n[PASS] testFuzz_RejectsUnsupportedTokens(address) (runs: 512, μ: 52161, ~: 52221)\n[PASS] testFuzz_RejectsWrongStake(bool,uint256) (runs: 512, μ: 78635, ~: 78593)\n[PASS] test_AllTiersBothTokensBothWinnersAndDraw() (gas: 12636261)\n[PASS] test_CannotSettleUnjoinedMatch() (gas: 239539)\n[PASS] test_ConcurrentMatchesAndDonationsCannotChangeAPot() (gas: 1298370)\n[PASS] test_ConstructorRejectsZeroOwnerOrZeroInitialSigner() (gas: 4015)\n[PASS] test_DeploymentOnEmptyChainHasFixedConfiguration() (gas: 356304)\n[PASS] test_InviteOnlyMatchRejectsSlotSnipersAndSettlesForTheInvitee() (gas: 709623)\n[PASS] test_InviteOnlyOpenRefundsAndPausesLikeAPublicOpen() (gas: 432192)\n[PASS] test_InviteeMayUsePlainJoinAndOpenMatchesStayPublic() (gas: 721093)\n[PASS] test_JoinCheckedRejectsEveryMismatchAndAcceptsTheExactMatch() (gas: 711240)\n[PASS] test_JoinClosesAtExactOneHourBoundary() (gas: 299415)\n[PASS] test_JoinedRefundWorksWhilePaused() (gas: 441132)\n[PASS] test_OpenAndJoinRecordExactDepositsAndEvents() (gas: 480700)\n[PASS] test_OpenInviteRejectsZeroAndSelfInviteeAndUsedIds() (gas: 434595)\n[PASS] test_OwnerHasNoWithdrawalOwnershipTransferOrUpgradeSurface() (gas: 522530)\n[PASS] test_PauseOnlyPreventsNewOpensAndUnpauseRestoresThem() (gas: 976996)\n[PASS] test_RejectsDuplicateOpenSelfJoinAndThirdPlayer() (gas: 467673)\n[PASS] test_RejectsMissingMatches() (gas: 101886)\n[PASS] test_RejectsNativeEtherAndUnknownCalls() (gas: 60874)\n[PASS] test_RejectsWholeTokenAmountsAndOtherTokenTiers() (gas: 145160)\n[PASS] test_RuntimeMeetsProtectedOpcodeAndSizeFloor() (gas: 4235294)\n[PASS] test_SettledIdsCannotReopenRefundOrJoin() (gas: 514513)\n[PASS] test_SettlementSucceedsOneSecondBeforeTimeout() (gas: 444575)\n[PASS] test_SettlementWindowStartsAtJoinAndClosesAtExactBoundary() (gas: 494559)\n[PASS] test_SignerAndPauseEventsAndInvalidChanges() (gas: 204018)\n[PASS] test_SquattedIdCannotChangeACheckedJoinersTokenOrStake() (gas: 417967)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 44.63ms (285.14ms CPU time)\n\nRan 1 test for test/PinballEscrowInvariant.t.sol:PinballEscrowInvariantTest\n[PASS] invariant_StateAndBalancesAlwaysMatchTheIndependentModel() (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+------------------+-------+---------+----------╮\n| Contract      | Selector         | Calls | Reverts | Discards |\n+===============================================================+\n| EscrowHandler | advanceTime      | 822   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | donate           | 790   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | joinCheckedMatch | 778   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | joinMatch        | 794   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | openInviteMatch  | 842   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | openMatch        | 854   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | refundMatch      | 792   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | rotateSigner     | 818   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | settleMatch      | 860   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | togglePause      | 842   | 0       | 0        |\n╰---------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.25s (4.25s CPU time)\n\nRan 4 test suites in 4.25s (4.36s CPU time): 64 tests passed, 0 failed, 0 skipped (64 total tests)\n","passed":true},{"durationMs":48,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PinballEscrow.join(bytes32)\",\"PinballEscrow.joinChecked(bytes32,address,uint256,address)\",\"PinballEscrow.open(bytes32,address,uint256)\",\"PinballEscrow.openInvite(bytes32,address,uint256,address)\",\"PinballEscrow.pause()\",\"PinballEscrow.refund(bytes32)\",\"PinballEscrow.setSigner(address)\",\"PinballEscrow.settle(bytes32,address,bool,uint256,bytes)\",\"PinballEscrow.unpause()\"],\"files\":{\".gitignore\":4,\"LICENSE\":21,\"README.md\":144,\"docs/dependencies.json\":18,\"docs/security-review.md\":47,\"docs/validation.md\":34,\"foundry.toml\":20,\"remappings.txt\":2,\"src/PinballEscrow.sol\":256,\"test/PinballEscrow.t.sol\":562,\"test/PinballEscrowInvariant.t.sol\":350,\"test/PinballEscrowSignatures.t.sol\":239,\"test/PinballEscrowTokens.t.sol\":257,\"test/helpers/EscrowTestBase.sol\":121,\"test/helpers/MockToken.sol\":92,\"test/helpers/SignerMock.sol\":29},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1208,"exitCode":0,"name":"slither","output":"[medium/medium] unused-return at src/PinballEscrow.sol:146: PinballEscrow.settle(bytes32,address,bool,uint256,bytes) (src/PinballEscrow.sol#146-178) ignores return value by (recovered,error,None) = ECDSA.tryRecover(digest,signature) (src/PinballEscrow.sol#158)\n[low/medium] timestamp at src/PinballEscrow.sol:146: PinballEscrow.settle(bytes32,address,bool,uint256,bytes) (src/PinballEscrow.sol#146-178) uses timestamp for comparisons\n[low/medium] timestamp at src/PinballEscrow.sol:182: PinballEscrow.refund(bytes32) (src/PinballEscrow.sol#182-193) uses timestamp for comparisons\n[low/medium] timestamp at src/PinballEscrow.sol:237: PinballEscrow._join(bytes32) (src/PinballEscrow.sol#237-249) uses timestamp for comparisons\n[low/medium] timestamp at src/PinballEscrow.sol:217: PinballEscrow._open(bytes32,address,uint256,address) (src/PinballEscrow.sol#217-235) uses timestamp for comparisons\n[low/medium] timestamp at src/PinballEscrow.sol:251: PinballEscrow._deposit(address,address,uint256) (src/PinballEscrow.sol#251-255) uses timestamp for comparisons","passed":true},{"durationMs":369,"exitCode":0,"name":"aderyn","output":"[low] centralization-risk at src/PinballEscrow.sol:196: Centralization Risk (3 places)\n[low] large-numeric-literal at src/PinballEscrow.sol:22: Large Numeric Literal","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"073b86f1f2ccc86346aa669e54e36266d0cbb55d103dbb19e7efaeb12183da74","verifiedTreeHash":"d281ca822ed8bfd31ce4f3e5175ba76c14fd5036","verifierVersion":"0.1.0+a6ee6548"},{"checks":[{"durationMs":3251,"exitCode":0,"name":"build","output":"Compiling 57 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.17s\nCompiler run successful!\n","passed":true},{"durationMs":13142,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 16 tests for test/PinballEscrowSignatures.t.sol:PinballEscrowSignaturesTest\n[PASS] testFuzz_SignerCannotAwardPotToAnOutsider(address) (runs: 512, μ: 364512, ~: 364570)\n[PASS] test_CannotReplayAcrossContracts() (gas: 972399)\n[PASS] test_ChainIdChangeRejectsOldSignatureAndAcceptsNewDomain() (gas: 481648)\n[PASS] test_DomainAndResultDigestMatchIndependentEncoding() (gas: 31270)\n[PASS] test_DrawCannotBeSettledAgain() (gas: 445590)\n[PASS] test_DrawRequiresCanonicalNoWinner() (gas: 441864)\n[PASS] test_ECDSASignerWithCodeCanStillSignDirectly() (gas: 444118)\n[PASS] test_ERC1271SignerAcceptsValidSignatureAndRejectsRevocationAndReverts() (gas: 1054975)\n[PASS] test_ExpiredSignatureRejectedButExactDeadlineAccepted() (gas: 457378)\n[PASS] test_MalformedInvalidVAndHighSSignaturesRejected() (gas: 692830)\n[PASS] test_MatchIdWinnerDrawAndDeadlineAreAllBound() (gas: 1037821)\n[PASS] test_ReplayAndFreshConflictingResultCannotDoubleSettle() (gas: 490759)\n[PASS] test_SignerRotationInvalidatesOutstandingOldKeyResults() (gas: 502880)\n[PASS] test_UnavailableSignerCannotPreventTimedRefund() (gas: 797022)\n[PASS] test_WrongDomainNameVersionAndPersonalSignAreRejected() (gas: 481843)\n[PASS] test_WrongSignerIsRejected() (gas: 404797)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 90.20ms (101.77ms CPU time)\n\nRan 32 tests for test/PinballEscrow.t.sol:PinballEscrowTest\n[PASS] testFuzz_AllowedStakesConserveFunds(bool,uint8,uint8,bytes32) (runs: 512, μ: 538108, ~: 542761)\n[PASS] testFuzz_BothRefundPathsBothTokens(bool,bool,uint8) (runs: 512, μ: 523122, ~: 461241)\n[PASS] testFuzz_JoinCheckedBindsOnlyTheExactParameters(bool,uint8,address,uint256,address) (runs: 512, μ: 273909, ~: 273890)\n[PASS] testFuzz_OnlyOwnerCanAdminister(address) (runs: 512, μ: 76101, ~: 76156)\n[PASS] testFuzz_OwnerFollowsConstructorArgumentNotDeployerOrOrigin(address,address) (runs: 512, μ: 368372, ~: 368462)\n[PASS] testFuzz_RejectsUnsupportedTokens(address) (runs: 512, μ: 52161, ~: 52221)\n[PASS] testFuzz_RejectsWrongStake(bool,uint256) (runs: 512, μ: 78636, ~: 78588)\n[PASS] test_AllTiersBothTokensBothWinnersAndDraw() (gas: 12636261)\n[PASS] test_CannotSettleUnjoinedMatch() (gas: 239539)\n[PASS] test_ConcurrentMatchesAndDonationsCannotChangeAPot() (gas: 1298370)\n[PASS] test_ConstructorRejectsZeroOwnerOrZeroInitialSigner() (gas: 4015)\n[PASS] test_DeploymentOnEmptyChainHasFixedConfiguration() (gas: 356304)\n[PASS] test_InviteOnlyMatchRejectsSlotSnipersAndSettlesForTheInvitee() (gas: 709623)\n[PASS] test_InviteOnlyOpenRefundsAndPausesLikeAPublicOpen() (gas: 432192)\n[PASS] test_InviteeMayUsePlainJoinAndOpenMatchesStayPublic() (gas: 721093)\n[PASS] test_JoinCheckedRejectsEveryMismatchAndAcceptsTheExactMatch() (gas: 711240)\n[PASS] test_JoinClosesAtExactOneHourBoundary() (gas: 299415)\n[PASS] test_JoinedRefundWorksWhilePaused() (gas: 441132)\n[PASS] test_OpenAndJoinRecordExactDepositsAndEvents() (gas: 480700)\n[PASS] test_OpenInviteRejectsZeroAndSelfInviteeAndUsedIds() (gas: 434595)\n[PASS] test_OwnerHasNoWithdrawalOwnershipTransferOrUpgradeSurface() (gas: 522530)\n[PASS] test_PauseOnlyPreventsNewOpensAndUnpauseRestoresThem() (gas: 976996)\n[PASS] test_RejectsDuplicateOpenSelfJoinAndThirdPlayer() (gas: 467673)\n[PASS] test_RejectsMissingMatches() (gas: 101886)\n[PASS] test_RejectsNativeEtherAndUnknownCalls() (gas: 60874)\n[PASS] test_RejectsWholeTokenAmountsAndOtherTokenTiers() (gas: 145160)\n[PASS] test_RuntimeMeetsProtectedOpcodeAndSizeFloor() (gas: 4235294)\n[PASS] test_SettledIdsCannotReopenRefundOrJoin() (gas: 514513)\n[PASS] test_SettlementSucceedsOneSecondBeforeTimeout() (gas: 444575)\n[PASS] test_SettlementWindowStartsAtJoinAndClosesAtExactBoundary() (gas: 494559)\n[PASS] test_SignerAndPauseEventsAndInvalidChanges() (gas: 204018)\n[PASS] test_SquattedIdCannotChangeACheckedJoinersTokenOrStake() (gas: 417967)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 94.68ms (560.98ms CPU time)\n\nRan 15 tests for test/PinballEscrowTokens.t.sol:PinballEscrowTokensTest\n[PASS] test_DrawSecondTransferFailureAlsoRollsBackFirstRefund() (gas: 596782)\n[PASS] test_FalseReturnAndRevertingDepositsCannotCreateOrJoinMatches() (gas: 915655)\n[PASS] test_FeeTransferFailureAlsoRollsBackWinnerPayment() (gas: 579052)\n[PASS] test_InexactDepositsRollbackStateBalancesAndAllowances() (gas: 1287874)\n[PASS] test_InsufficientBalanceDoesNotReserveAnIdOrPlayerSlot() (gas: 700297)\n[PASS] test_MissingAllowanceRollsBackOpenAndJoin() (gas: 824062)\n[PASS] test_MissingTokenCodeFailsOnUseAndLeavesMatchUncreated() (gas: 186812)\n[PASS] test_ReentrancyCannotMoveAnotherMatchsDeposits() (gas: 961478)\n[PASS] test_ReentrancyDuringOpenAndJoinIsRejectedWithEffectsAlreadyApplied() (gas: 749268)\n[PASS] test_ReentrancyDuringRefundCannotRefundAgain() (gas: 680731)\n[PASS] test_ReentrancyDuringSettlementCannotSettleTwiceOrOpenAnotherMatch() (gas: 1451200)\n[PASS] test_RefundFailureIsAtomicAndDoesNotBlockUnrelatedMatches() (gas: 834433)\n[PASS] test_SafeERC20SupportsTokensWithoutReturnValues() (gas: 517556)\n[PASS] test_UnjoinedRefundFailureCanBeRetried() (gas: 405331)\n[PASS] test_WinnerTransferFailureIsAtomicAndRetryable() (gas: 609436)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 100.27ms (9.30ms CPU time)\n\nRan 6 tests for test/PinballEscrowEdges.t.sol:PinballEscrowEdgesTest\n[PASS] testFuzz_ApprovalForTheOtherTokenCannotFundAJoin(bool,uint8) (runs: 512, μ: 614313, ~: 614314)\n[PASS] testFuzz_ArbitraryInviteeIsTheOnlyPossibleOpponent(address,uint256,bool,uint8,uint256) (runs: 512, μ: 871453, ~: 871493)\nLogs:\n  Bound result 2434\n\n[PASS] testFuzz_EntireBalancesAndExactAllowancesCanFundAndExit(bool,uint8,uint8,uint256) (runs: 1000, μ: 720674, ~: 750649)\nLogs:\n  Bound result 2\n  Bound result 2978\n\n[PASS] testFuzz_MalformedSignatureLengthsCannotReleaseAJoinedPot(bytes,bool) (runs: 512, μ: 508224, ~: 507976)\n[PASS] test_AllExitPaymentFailuresBothTokensAreAtomicAndRetryable() (gas: 38246795)\n[PASS] test_EveryStakeNeighbourAndIntegerExtremeIsRejectedWithoutTakingFunds() (gas: 2869101)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 100.26ms (373.54ms CPU time)\n\nRan 1 test for test/PinballEscrowInvariant.t.sol:PinballEscrowInvariantTest\n[PASS] invariant_StateAndBalancesAlwaysMatchTheIndependentModel() (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+------------------+-------+---------+----------╮\n| Contract      | Selector         | Calls | Reverts | Discards |\n+===============================================================+\n| EscrowHandler | advanceTime      | 822   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | donate           | 790   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | joinCheckedMatch | 778   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | joinMatch        | 794   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | openInviteMatch  | 842   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | openMatch        | 854   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | refundMatch      | 792   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | rotateSigner     | 818   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | settleMatch      | 860   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | togglePause      | 842   | 0       | 0        |\n╰---------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.62s (3.62s CPU time)\n\nRan 3 tests for test/PinballEscrowContinuousInvariant.t.sol:PinballEscrowContinuousInvariantTest\n[PASS] invariant_ContinuousCustodyAndRecovery() (runs: 256, calls: 32768, reverts: 0)\n\n╭-------------------------+----------------------+-------+---------+----------╮\n| Contract                | Selector             | Calls | Reverts | Discards |\n+=============================================================================+\n| ContinuousEscrowHandler | administer           | 3606  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | advanceTime          | 3698  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | donate               | 3627  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | failPaymentThenRetry | 3518  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | intrude              | 3688  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | progress             | 3610  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | progressInvite       | 3677  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | rejectDeposit        | 3679  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | replayFinished       | 3665  | 0       | 0        |\n╰-------------------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1358\n  Bound result 1050\n  Bound result 871\n  Bound result 321223719875344982\n  Bound result 0\n  Bound result 7098\n  Bound result 9955\n  Bound result 3181\n  Bound result 999999989900500000\n  Bound result 2928\n  Bound result 24229\n  Bound result 6333\n  Bound result 13376\n\n[PASS] test_HandlerExercisesFailuresFreshMatchesAndEveryExit() (gas: 11412131)\nLogs:\n  Bound result 1\n\n[PASS] test_HandlerExercisesInvitesCheckedJoinsAndIntrusions() (gas: 7611826)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 13.05s (13.06s CPU time)\n\nRan 6 test suites in 13.05s (17.06s CPU time): 73 tests passed, 0 failed, 0 skipped (73 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PinballEscrow.join(bytes32)\",\"PinballEscrow.joinChecked(bytes32,address,uint256,address)\",\"PinballEscrow.open(bytes32,address,uint256)\",\"PinballEscrow.openInvite(bytes32,address,uint256,address)\",\"PinballEscrow.pause()\",\"PinballEscrow.refund(bytes32)\",\"PinballEscrow.setSigner(address)\",\"PinballEscrow.settle(bytes32,address,bool,uint256,bytes)\",\"PinballEscrow.unpause()\"],\"files\":{\".gitignore\":4,\"LICENSE\":21,\"README.md\":144,\"docs/dependencies.json\":18,\"docs/security-review.md\":47,\"docs/validation.md\":34,\"foundry.toml\":20,\"remappings.txt\":2,\"src/PinballEscrow.sol\":256,\"test/CONTRIBUTOR-TESTS.md\":75,\"test/PinballEscrow.t.sol\":562,\"test/PinballEscrowContinuousInvariant.t.sol\":121,\"test/PinballEscrowEdges.t.sol\":227,\"test/PinballEscrowInvariant.t.sol\":351,\"test/PinballEscrowSignatures.t.sol\":239,\"test/PinballEscrowTokens.t.sol\":257,\"test/helpers/ContinuousEscrowHandler.sol\":439,\"test/helpers/EscrowTestBase.sol\":121,\"test/helpers/MockToken.sol\":92,\"test/helpers/SignerMock.sol\":29},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"6f50f51236405f70b2512eb153c13dbfce93966ca8ba3655a652f7cf518d53a7","verifiedTreeHash":"ec8cc16b8ec002e60310057efd2fe170d0c56881","verifierVersion":"0.1.0+a6ee6548"},{"checks":[{"durationMs":2318,"exitCode":0,"name":"build","output":"Compiling 54 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.24s\nCompiler run successful!\n","passed":true},{"durationMs":4134,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 16 tests for test/PinballEscrowSignatures.t.sol:PinballEscrowSignaturesTest\n[PASS] testFuzz_SignerCannotAwardPotToAnOutsider(address) (runs: 512, μ: 364512, ~: 364570)\n[PASS] test_CannotReplayAcrossContracts() (gas: 972399)\n[PASS] test_ChainIdChangeRejectsOldSignatureAndAcceptsNewDomain() (gas: 481648)\n[PASS] test_DomainAndResultDigestMatchIndependentEncoding() (gas: 31270)\n[PASS] test_DrawCannotBeSettledAgain() (gas: 445590)\n[PASS] test_DrawRequiresCanonicalNoWinner() (gas: 441864)\n[PASS] test_ECDSASignerWithCodeCanStillSignDirectly() (gas: 444118)\n[PASS] test_ERC1271SignerAcceptsValidSignatureAndRejectsRevocationAndReverts() (gas: 1054975)\n[PASS] test_ExpiredSignatureRejectedButExactDeadlineAccepted() (gas: 457378)\n[PASS] test_MalformedInvalidVAndHighSSignaturesRejected() (gas: 692830)\n[PASS] test_MatchIdWinnerDrawAndDeadlineAreAllBound() (gas: 1037821)\n[PASS] test_ReplayAndFreshConflictingResultCannotDoubleSettle() (gas: 490759)\n[PASS] test_SignerRotationInvalidatesOutstandingOldKeyResults() (gas: 502880)\n[PASS] test_UnavailableSignerCannotPreventTimedRefund() (gas: 797022)\n[PASS] test_WrongDomainNameVersionAndPersonalSignAreRejected() (gas: 481843)\n[PASS] test_WrongSignerIsRejected() (gas: 404797)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 36.75ms (45.61ms CPU time)\n\nRan 15 tests for test/PinballEscrowTokens.t.sol:PinballEscrowTokensTest\n[PASS] test_DrawSecondTransferFailureAlsoRollsBackFirstRefund() (gas: 596782)\n[PASS] test_FalseReturnAndRevertingDepositsCannotCreateOrJoinMatches() (gas: 915655)\n[PASS] test_FeeTransferFailureAlsoRollsBackWinnerPayment() (gas: 579052)\n[PASS] test_InexactDepositsRollbackStateBalancesAndAllowances() (gas: 1287874)\n[PASS] test_InsufficientBalanceDoesNotReserveAnIdOrPlayerSlot() (gas: 700297)\n[PASS] test_MissingAllowanceRollsBackOpenAndJoin() (gas: 824062)\n[PASS] test_MissingTokenCodeFailsOnUseAndLeavesMatchUncreated() (gas: 186812)\n[PASS] test_ReentrancyCannotMoveAnotherMatchsDeposits() (gas: 961478)\n[PASS] test_ReentrancyDuringOpenAndJoinIsRejectedWithEffectsAlreadyApplied() (gas: 749268)\n[PASS] test_ReentrancyDuringRefundCannotRefundAgain() (gas: 680731)\n[PASS] test_ReentrancyDuringSettlementCannotSettleTwiceOrOpenAnotherMatch() (gas: 1451200)\n[PASS] test_RefundFailureIsAtomicAndDoesNotBlockUnrelatedMatches() (gas: 834433)\n[PASS] test_SafeERC20SupportsTokensWithoutReturnValues() (gas: 517556)\n[PASS] test_UnjoinedRefundFailureCanBeRetried() (gas: 405331)\n[PASS] test_WinnerTransferFailureIsAtomicAndRetryable() (gas: 609436)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 37.55ms (6.67ms CPU time)\n\nRan 32 tests for test/PinballEscrow.t.sol:PinballEscrowTest\n[PASS] testFuzz_AllowedStakesConserveFunds(bool,uint8,uint8,bytes32) (runs: 512, μ: 538192, ~: 542760)\n[PASS] testFuzz_BothRefundPathsBothTokens(bool,bool,uint8) (runs: 512, μ: 523123, ~: 461241)\n[PASS] testFuzz_JoinCheckedBindsOnlyTheExactParameters(bool,uint8,address,uint256,address) (runs: 512, μ: 273907, ~: 273888)\n[PASS] testFuzz_OnlyOwnerCanAdminister(address) (runs: 512, μ: 76101, ~: 76156)\n[PASS] testFuzz_OwnerFollowsConstructorArgumentNotDeployerOrOrigin(address,address) (runs: 512, μ: 368372, ~: 368462)\n[PASS] testFuzz_RejectsUnsupportedTokens(address) (runs: 512, μ: 52161, ~: 52221)\n[PASS] testFuzz_RejectsWrongStake(bool,uint256) (runs: 512, μ: 78635, ~: 78593)\n[PASS] test_AllTiersBothTokensBothWinnersAndDraw() (gas: 12636261)\n[PASS] test_CannotSettleUnjoinedMatch() (gas: 239539)\n[PASS] test_ConcurrentMatchesAndDonationsCannotChangeAPot() (gas: 1298370)\n[PASS] test_ConstructorRejectsZeroOwnerOrZeroInitialSigner() (gas: 4015)\n[PASS] test_DeploymentOnEmptyChainHasFixedConfiguration() (gas: 356304)\n[PASS] test_InviteOnlyMatchRejectsSlotSnipersAndSettlesForTheInvitee() (gas: 709623)\n[PASS] test_InviteOnlyOpenRefundsAndPausesLikeAPublicOpen() (gas: 432192)\n[PASS] test_InviteeMayUsePlainJoinAndOpenMatchesStayPublic() (gas: 721093)\n[PASS] test_JoinCheckedRejectsEveryMismatchAndAcceptsTheExactMatch() (gas: 711240)\n[PASS] test_JoinClosesAtExactOneHourBoundary() (gas: 299415)\n[PASS] test_JoinedRefundWorksWhilePaused() (gas: 441132)\n[PASS] test_OpenAndJoinRecordExactDepositsAndEvents() (gas: 480700)\n[PASS] test_OpenInviteRejectsZeroAndSelfInviteeAndUsedIds() (gas: 434595)\n[PASS] test_OwnerHasNoWithdrawalOwnershipTransferOrUpgradeSurface() (gas: 522530)\n[PASS] test_PauseOnlyPreventsNewOpensAndUnpauseRestoresThem() (gas: 976996)\n[PASS] test_RejectsDuplicateOpenSelfJoinAndThirdPlayer() (gas: 467673)\n[PASS] test_RejectsMissingMatches() (gas: 101886)\n[PASS] test_RejectsNativeEtherAndUnknownCalls() (gas: 60874)\n[PASS] test_RejectsWholeTokenAmountsAndOtherTokenTiers() (gas: 145160)\n[PASS] test_RuntimeMeetsProtectedOpcodeAndSizeFloor() (gas: 4235294)\n[PASS] test_SettledIdsCannotReopenRefundOrJoin() (gas: 514513)\n[PASS] test_SettlementSucceedsOneSecondBeforeTimeout() (gas: 444575)\n[PASS] test_SettlementWindowStartsAtJoinAndClosesAtExactBoundary() (gas: 494559)\n[PASS] test_SignerAndPauseEventsAndInvalidChanges() (gas: 204018)\n[PASS] test_SquattedIdCannotChangeACheckedJoinersTokenOrStake() (gas: 417967)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 48.62ms (293.84ms CPU time)\n\nRan 1 test for test/PinballEscrowInvariant.t.sol:PinballEscrowInvariantTest\n[PASS] invariant_StateAndBalancesAlwaysMatchTheIndependentModel() (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+------------------+-------+---------+----------╮\n| Contract      | Selector         | Calls | Reverts | Discards |\n+===============================================================+\n| EscrowHandler | advanceTime      | 822   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | donate           | 790   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | joinCheckedMatch | 778   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | joinMatch        | 794   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | openInviteMatch  | 842   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | openMatch        | 854   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | refundMatch      | 792   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | rotateSigner     | 818   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | settleMatch      | 860   | 0       | 0        |\n|---------------+------------------+-------+---------+----------|\n| EscrowHandler | togglePause      | 842   | 0       | 0        |\n╰---------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.05s (4.05s CPU time)\n\nRan 4 test suites in 4.06s (4.18s CPU time): 64 tests passed, 0 failed, 0 skipped (64 total tests)\n","passed":true},{"durationMs":39,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PinballEscrow.join(bytes32)\",\"PinballEscrow.joinChecked(bytes32,address,uint256,address)\",\"PinballEscrow.open(bytes32,address,uint256)\",\"PinballEscrow.openInvite(bytes32,address,uint256,address)\",\"PinballEscrow.pause()\",\"PinballEscrow.refund(bytes32)\",\"PinballEscrow.setSigner(address)\",\"PinballEscrow.settle(bytes32,address,bool,uint256,bytes)\",\"PinballEscrow.unpause()\"],\"files\":{\".gitignore\":4,\"LICENSE\":21,\"README.md\":144,\"docs/dependencies.json\":18,\"docs/security-review.md\":47,\"docs/validation.md\":34,\"foundry.toml\":20,\"launch.json\":13,\"remappings.txt\":2,\"src/PinballEscrow.sol\":256,\"test/PinballEscrow.t.sol\":562,\"test/PinballEscrowInvariant.t.sol\":350,\"test/PinballEscrowSignatures.t.sol\":239,\"test/PinballEscrowTokens.t.sol\":257,\"test/helpers/EscrowTestBase.sol\":121,\"test/helpers/MockToken.sol\":92,\"test/helpers/SignerMock.sol\":29},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"836710b9b89d0424e7c8b68a6db933f4c0fb0a61cca155f085e4393e5cd7613f","verifiedTreeHash":"f325bf1ce45c1fe044363f86ebab3f99e8e46829","verifierVersion":"0.1.0+a6ee6548"},{"checks":[{"durationMs":2256,"exitCode":0,"name":"build","output":"Compiling 54 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.13s\nCompiler run successful!\n","passed":true},{"durationMs":3978,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 16 tests for test/PinballEscrowSignatures.t.sol:PinballEscrowSignaturesTest\n[PASS] testFuzz_SignerCannotAwardPotToAnOutsider(address) (runs: 512, μ: 359858, ~: 359909)\n[PASS] test_CannotReplayAcrossContracts() (gas: 962870)\n[PASS] test_ChainIdChangeRejectsOldSignatureAndAcceptsNewDomain() (gas: 476943)\n[PASS] test_DomainAndResultDigestMatchIndependentEncoding() (gas: 31226)\n[PASS] test_DrawCannotBeSettledAgain() (gas: 440907)\n[PASS] test_DrawRequiresCanonicalNoWinner() (gas: 437181)\n[PASS] test_ECDSASignerWithCodeCanStillSignDirectly() (gas: 436985)\n[PASS] test_ERC1271SignerAcceptsValidSignatureAndRejectsRevocationAndReverts() (gas: 1050208)\n[PASS] test_ExpiredSignatureRejectedButExactDeadlineAccepted() (gas: 452695)\n[PASS] test_MalformedInvalidVAndHighSSignaturesRejected() (gas: 688037)\n[PASS] test_MatchIdWinnerDrawAndDeadlineAreAllBound() (gas: 1028389)\n[PASS] test_ReplayAndFreshConflictingResultCannotDoubleSettle() (gas: 486054)\n[PASS] test_SignerRotationInvalidatesOutstandingOldKeyResults() (gas: 498157)\n[PASS] test_UnavailableSignerCannotPreventTimedRefund() (gas: 792321)\n[PASS] test_WrongDomainNameVersionAndPersonalSignAreRejected() (gas: 477138)\n[PASS] test_WrongSignerIsRejected() (gas: 397663)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 25.71ms (35.56ms CPU time)\n\nRan 15 tests for test/PinballEscrowTokens.t.sol:PinballEscrowTokensTest\n[PASS] test_DrawSecondTransferFailureAlsoRollsBackFirstRefund() (gas: 589734)\n[PASS] test_FalseReturnAndRevertingDepositsCannotCreateOrJoinMatches() (gas: 901628)\n[PASS] test_FeeTransferFailureAlsoRollsBackWinnerPayment() (gas: 572004)\n[PASS] test_InexactDepositsRollbackStateBalancesAndAllowances() (gas: 1266749)\n[PASS] test_InsufficientBalanceDoesNotReserveAnIdOrPlayerSlot() (gas: 686316)\n[PASS] test_MissingAllowanceRollsBackOpenAndJoin() (gas: 807715)\n[PASS] test_MissingTokenCodeFailsOnUseAndLeavesMatchUncreated() (gas: 182023)\n[PASS] test_ReentrancyCannotMoveAnotherMatchsDeposits() (gas: 949495)\n[PASS] test_ReentrancyDuringOpenAndJoinIsRejectedWithEffectsAlreadyApplied() (gas: 741295)\n[PASS] test_ReentrancyDuringRefundCannotRefundAgain() (gas: 673575)\n[PASS] test_ReentrancyDuringSettlementCannotSettleTwiceOrOpenAnotherMatch() (gas: 1434545)\n[PASS] test_RefundFailureIsAtomicAndDoesNotBlockUnrelatedMatches() (gas: 824923)\n[PASS] test_SafeERC20SupportsTokensWithoutReturnValues() (gas: 512895)\n[PASS] test_UnjoinedRefundFailureCanBeRetried() (gas: 400482)\n[PASS] test_WinnerTransferFailureIsAtomicAndRetryable() (gas: 602388)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 32.09ms (12.84ms CPU time)\n\nRan 24 tests for test/PinballEscrow.t.sol:PinballEscrowTest\n[PASS] testFuzz_AllowedStakesConserveFunds(bool,uint8,uint8,bytes32) (runs: 512, μ: 531271, ~: 535626)\n[PASS] testFuzz_BothRefundPathsBothTokens(bool,bool,uint8) (runs: 512, μ: 517067, ~: 456230)\n[PASS] testFuzz_OnlyOwnerCanAdminister(address) (runs: 512, μ: 75987, ~: 76036)\n[PASS] testFuzz_RejectsUnsupportedTokens(address) (runs: 512, μ: 52141, ~: 52194)\n[PASS] testFuzz_RejectsWrongStake(bool,uint256) (runs: 512, μ: 76115, ~: 76072)\n[PASS] test_AllTiersBothTokensBothWinnersAndDraw() (gas: 12464797)\n[PASS] test_CannotSettleUnjoinedMatch() (gas: 237055)\n[PASS] test_ConcurrentMatchesAndDonationsCannotChangeAPot() (gas: 1284277)\n[PASS] test_ConstructorRejectsIncorrectOrZeroInitialSigner() (gas: 3901)\n[PASS] test_DeploymentOnEmptyChainHasFixedConfiguration() (gas: 332114)\n[PASS] test_JoinClosesAtExactOneHourBoundary() (gas: 294787)\n[PASS] test_JoinedRefundWorksWhilePaused() (gas: 436453)\n[PASS] test_OpenAndJoinRecordExactDepositsAndEvents() (gas: 466084)\n[PASS] test_OwnerHasNoWithdrawalOwnershipTransferOrUpgradeSurface() (gas: 517929)\n[PASS] test_PauseOnlyPreventsNewOpensAndUnpauseRestoresThem() (gas: 967352)\n[PASS] test_RejectsDuplicateOpenSelfJoinAndThirdPlayer() (gas: 462887)\n[PASS] test_RejectsMissingMatches() (gas: 101846)\n[PASS] test_RejectsNativeEtherAndUnknownCalls() (gas: 60806)\n[PASS] test_RejectsWholeTokenAmountsAndOtherTokenTiers() (gas: 145034)\n[PASS] test_RuntimeMeetsProtectedOpcodeAndSizeFloor() (gas: 3829057)\n[PASS] test_SettledIdsCannotReopenRefundOrJoin() (gas: 509808)\n[PASS] test_SettlementSucceedsOneSecondBeforeTimeout() (gas: 437419)\n[PASS] test_SettlementWindowStartsAtJoinAndClosesAtExactBoundary() (gas: 489854)\n[PASS] test_SignerAndPauseEventsAndInvalidChanges() (gas: 203800)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 37.82ms (198.09ms CPU time)\n\nRan 1 test for test/PinballEscrowInvariant.t.sol:PinballEscrowInvariantTest\n[PASS] invariant_StateAndBalancesAlwaysMatchTheIndependentModel() (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+--------------+-------+---------+----------╮\n| Contract      | Selector     | Calls | Reverts | Discards |\n+===========================================================+\n| EscrowHandler | advanceTime  | 1010  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | donate       | 977   | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | joinMatch    | 1025  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | openMatch    | 1081  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | refundMatch  | 1016  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | rotateSigner | 1044  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | settleMatch  | 1024  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | togglePause  | 1015  | 0       | 0        |\n╰---------------+--------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.87s (3.87s CPU time)\n\nRan 4 test suites in 3.87s (3.96s CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PinballEscrow.join(bytes32)\",\"PinballEscrow.open(bytes32,address,uint256)\",\"PinballEscrow.pause()\",\"PinballEscrow.refund(bytes32)\",\"PinballEscrow.setSigner(address)\",\"PinballEscrow.settle(bytes32,address,bool,uint256,bytes)\",\"PinballEscrow.unpause()\"],\"files\":{\".gitignore\":4,\"LICENSE\":21,\"README.md\":132,\"docs/dependencies.json\":18,\"docs/security-review.md\":46,\"docs/validation.md\":30,\"foundry.toml\":20,\"remappings.txt\":2,\"src/PinballEscrow.sol\":213,\"test/PinballEscrow.t.sol\":392,\"test/PinballEscrowInvariant.t.sol\":290,\"test/PinballEscrowSignatures.t.sol\":239,\"test/PinballEscrowTokens.t.sol\":257,\"test/helpers/EscrowTestBase.sol\":121,\"test/helpers/MockToken.sol\":92,\"test/helpers/SignerMock.sol\":29},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1178,"exitCode":0,"name":"slither","output":"[medium/medium] unused-return at src/PinballEscrow.sol:137: PinballEscrow.settle(bytes32,address,bool,uint256,bytes) (src/PinballEscrow.sol#137-169) ignores return value by (recovered,error,None) = ECDSA.tryRecover(digest,signature) (src/PinballEscrow.sol#149)\n[low/medium] timestamp at src/PinballEscrow.sol:121: PinballEscrow.join(bytes32) (src/PinballEscrow.sol#121-132) uses timestamp for comparisons\n[low/medium] timestamp at src/PinballEscrow.sol:208: PinballEscrow._deposit(address,address,uint256) (src/PinballEscrow.sol#208-212) uses timestamp for comparisons\n[low/medium] timestamp at src/PinballEscrow.sol:137: PinballEscrow.settle(bytes32,address,bool,uint256,bytes) (src/PinballEscrow.sol#137-169) uses timestamp for comparisons\n[low/medium] timestamp at src/PinballEscrow.sol:173: PinballEscrow.refund(bytes32) (src/PinballEscrow.sol#173-184) uses timestamp for comparisons","passed":true},{"durationMs":402,"exitCode":0,"name":"aderyn","output":"[low] centralization-risk at src/PinballEscrow.sol:187: Centralization Risk (3 places)\n[low] large-numeric-literal at src/PinballEscrow.sol:23: Large Numeric Literal","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"b29525166dcdeb1396f98d8b41e2dcdc0bd4a8ae64c908a1a33d9ccffafc4828","verifiedTreeHash":"84755c304b253ced47b3023a352b760d742582dc","verifierVersion":"0.1.0+a481cd14"},{"checks":[{"durationMs":2347,"exitCode":0,"name":"build","output":"Compiling 54 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.26s\nCompiler run successful!\n","passed":true},{"durationMs":3854,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 16 tests for test/PinballEscrowSignatures.t.sol:PinballEscrowSignaturesTest\n[PASS] testFuzz_SignerCannotAwardPotToAnOutsider(address) (runs: 512, μ: 359858, ~: 359909)\n[PASS] test_CannotReplayAcrossContracts() (gas: 962870)\n[PASS] test_ChainIdChangeRejectsOldSignatureAndAcceptsNewDomain() (gas: 476943)\n[PASS] test_DomainAndResultDigestMatchIndependentEncoding() (gas: 31226)\n[PASS] test_DrawCannotBeSettledAgain() (gas: 440907)\n[PASS] test_DrawRequiresCanonicalNoWinner() (gas: 437181)\n[PASS] test_ECDSASignerWithCodeCanStillSignDirectly() (gas: 436985)\n[PASS] test_ERC1271SignerAcceptsValidSignatureAndRejectsRevocationAndReverts() (gas: 1050208)\n[PASS] test_ExpiredSignatureRejectedButExactDeadlineAccepted() (gas: 452695)\n[PASS] test_MalformedInvalidVAndHighSSignaturesRejected() (gas: 688037)\n[PASS] test_MatchIdWinnerDrawAndDeadlineAreAllBound() (gas: 1028389)\n[PASS] test_ReplayAndFreshConflictingResultCannotDoubleSettle() (gas: 486054)\n[PASS] test_SignerRotationInvalidatesOutstandingOldKeyResults() (gas: 498157)\n[PASS] test_UnavailableSignerCannotPreventTimedRefund() (gas: 792321)\n[PASS] test_WrongDomainNameVersionAndPersonalSignAreRejected() (gas: 477138)\n[PASS] test_WrongSignerIsRejected() (gas: 397663)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 28.39ms (34.14ms CPU time)\n\nRan 15 tests for test/PinballEscrowTokens.t.sol:PinballEscrowTokensTest\n[PASS] test_DrawSecondTransferFailureAlsoRollsBackFirstRefund() (gas: 589734)\n[PASS] test_FalseReturnAndRevertingDepositsCannotCreateOrJoinMatches() (gas: 901628)\n[PASS] test_FeeTransferFailureAlsoRollsBackWinnerPayment() (gas: 572004)\n[PASS] test_InexactDepositsRollbackStateBalancesAndAllowances() (gas: 1266749)\n[PASS] test_InsufficientBalanceDoesNotReserveAnIdOrPlayerSlot() (gas: 686316)\n[PASS] test_MissingAllowanceRollsBackOpenAndJoin() (gas: 807715)\n[PASS] test_MissingTokenCodeFailsOnUseAndLeavesMatchUncreated() (gas: 182023)\n[PASS] test_ReentrancyCannotMoveAnotherMatchsDeposits() (gas: 949495)\n[PASS] test_ReentrancyDuringOpenAndJoinIsRejectedWithEffectsAlreadyApplied() (gas: 741295)\n[PASS] test_ReentrancyDuringRefundCannotRefundAgain() (gas: 673575)\n[PASS] test_ReentrancyDuringSettlementCannotSettleTwiceOrOpenAnotherMatch() (gas: 1434545)\n[PASS] test_RefundFailureIsAtomicAndDoesNotBlockUnrelatedMatches() (gas: 824923)\n[PASS] test_SafeERC20SupportsTokensWithoutReturnValues() (gas: 512895)\n[PASS] test_UnjoinedRefundFailureCanBeRetried() (gas: 400482)\n[PASS] test_WinnerTransferFailureIsAtomicAndRetryable() (gas: 602388)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 34.38ms (9.57ms CPU time)\n\nRan 24 tests for test/PinballEscrow.t.sol:PinballEscrowTest\n[PASS] testFuzz_AllowedStakesConserveFunds(bool,uint8,uint8,bytes32) (runs: 512, μ: 531271, ~: 535626)\n[PASS] testFuzz_BothRefundPathsBothTokens(bool,bool,uint8) (runs: 512, μ: 517067, ~: 456230)\n[PASS] testFuzz_OnlyOwnerCanAdminister(address) (runs: 512, μ: 75987, ~: 76036)\n[PASS] testFuzz_RejectsUnsupportedTokens(address) (runs: 512, μ: 52141, ~: 52194)\n[PASS] testFuzz_RejectsWrongStake(bool,uint256) (runs: 512, μ: 76115, ~: 76072)\n[PASS] test_AllTiersBothTokensBothWinnersAndDraw() (gas: 12464797)\n[PASS] test_CannotSettleUnjoinedMatch() (gas: 237055)\n[PASS] test_ConcurrentMatchesAndDonationsCannotChangeAPot() (gas: 1284277)\n[PASS] test_ConstructorRejectsIncorrectOrZeroInitialSigner() (gas: 3901)\n[PASS] test_DeploymentOnEmptyChainHasFixedConfiguration() (gas: 332114)\n[PASS] test_JoinClosesAtExactOneHourBoundary() (gas: 294787)\n[PASS] test_JoinedRefundWorksWhilePaused() (gas: 436453)\n[PASS] test_OpenAndJoinRecordExactDepositsAndEvents() (gas: 466084)\n[PASS] test_OwnerHasNoWithdrawalOwnershipTransferOrUpgradeSurface() (gas: 517929)\n[PASS] test_PauseOnlyPreventsNewOpensAndUnpauseRestoresThem() (gas: 967352)\n[PASS] test_RejectsDuplicateOpenSelfJoinAndThirdPlayer() (gas: 462887)\n[PASS] test_RejectsMissingMatches() (gas: 101846)\n[PASS] test_RejectsNativeEtherAndUnknownCalls() (gas: 60806)\n[PASS] test_RejectsWholeTokenAmountsAndOtherTokenTiers() (gas: 145034)\n[PASS] test_RuntimeMeetsProtectedOpcodeAndSizeFloor() (gas: 3829057)\n[PASS] test_SettledIdsCannotReopenRefundOrJoin() (gas: 509808)\n[PASS] test_SettlementSucceedsOneSecondBeforeTimeout() (gas: 437419)\n[PASS] test_SettlementWindowStartsAtJoinAndClosesAtExactBoundary() (gas: 489854)\n[PASS] test_SignerAndPauseEventsAndInvalidChanges() (gas: 203800)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 34.44ms (185.69ms CPU time)\n\nRan 1 test for test/PinballEscrowInvariant.t.sol:PinballEscrowInvariantTest\n[PASS] invariant_StateAndBalancesAlwaysMatchTheIndependentModel() (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+--------------+-------+---------+----------╮\n| Contract      | Selector     | Calls | Reverts | Discards |\n+===========================================================+\n| EscrowHandler | advanceTime  | 1010  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | donate       | 977   | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | joinMatch    | 1025  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | openMatch    | 1081  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | refundMatch  | 1016  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | rotateSigner | 1044  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | settleMatch  | 1024  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | togglePause  | 1015  | 0       | 0        |\n╰---------------+--------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.76s (3.76s CPU time)\n\nRan 4 test suites in 3.76s (3.86s CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":36,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PinballEscrow.join(bytes32)\",\"PinballEscrow.open(bytes32,address,uint256)\",\"PinballEscrow.pause()\",\"PinballEscrow.refund(bytes32)\",\"PinballEscrow.setSigner(address)\",\"PinballEscrow.settle(bytes32,address,bool,uint256,bytes)\",\"PinballEscrow.unpause()\"],\"files\":{\".gitignore\":4,\"LICENSE\":21,\"README.md\":132,\"docs/dependencies.json\":18,\"docs/security-review.md\":46,\"docs/validation.md\":30,\"foundry.toml\":20,\"launch.json\":12,\"remappings.txt\":2,\"src/PinballEscrow.sol\":213,\"test/PinballEscrow.t.sol\":392,\"test/PinballEscrowInvariant.t.sol\":290,\"test/PinballEscrowSignatures.t.sol\":239,\"test/PinballEscrowTokens.t.sol\":257,\"test/helpers/EscrowTestBase.sol\":121,\"test/helpers/MockToken.sol\":92,\"test/helpers/SignerMock.sol\":29},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed; 1 finding(s) for the step that wrote the contracts","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"b967ca41d0250c79f1618308f402c35dc5d78d7cb595187eebab7bf8588457f0","verifiedTreeHash":"3dcb4ddd9bf183075a255fd132f354a79c40fc12","verifierVersion":"0.1.0+a481cd14"},{"checks":[{"durationMs":2854,"exitCode":0,"name":"build","output":"Compiling 57 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.78s\nCompiler run successful!\n","passed":true},{"durationMs":11981,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 15 tests for test/PinballEscrowTokens.t.sol:PinballEscrowTokensTest\n[PASS] test_DrawSecondTransferFailureAlsoRollsBackFirstRefund() (gas: 589734)\n[PASS] test_FalseReturnAndRevertingDepositsCannotCreateOrJoinMatches() (gas: 901628)\n[PASS] test_FeeTransferFailureAlsoRollsBackWinnerPayment() (gas: 572004)\n[PASS] test_InexactDepositsRollbackStateBalancesAndAllowances() (gas: 1266749)\n[PASS] test_InsufficientBalanceDoesNotReserveAnIdOrPlayerSlot() (gas: 686316)\n[PASS] test_MissingAllowanceRollsBackOpenAndJoin() (gas: 807715)\n[PASS] test_MissingTokenCodeFailsOnUseAndLeavesMatchUncreated() (gas: 182023)\n[PASS] test_ReentrancyCannotMoveAnotherMatchsDeposits() (gas: 949495)\n[PASS] test_ReentrancyDuringOpenAndJoinIsRejectedWithEffectsAlreadyApplied() (gas: 741295)\n[PASS] test_ReentrancyDuringRefundCannotRefundAgain() (gas: 673575)\n[PASS] test_ReentrancyDuringSettlementCannotSettleTwiceOrOpenAnotherMatch() (gas: 1434545)\n[PASS] test_RefundFailureIsAtomicAndDoesNotBlockUnrelatedMatches() (gas: 824923)\n[PASS] test_SafeERC20SupportsTokensWithoutReturnValues() (gas: 512895)\n[PASS] test_UnjoinedRefundFailureCanBeRetried() (gas: 400482)\n[PASS] test_WinnerTransferFailureIsAtomicAndRetryable() (gas: 602388)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 25.36ms (7.68ms CPU time)\n\nRan 16 tests for test/PinballEscrowSignatures.t.sol:PinballEscrowSignaturesTest\n[PASS] testFuzz_SignerCannotAwardPotToAnOutsider(address) (runs: 512, μ: 359858, ~: 359909)\n[PASS] test_CannotReplayAcrossContracts() (gas: 962870)\n[PASS] test_ChainIdChangeRejectsOldSignatureAndAcceptsNewDomain() (gas: 476943)\n[PASS] test_DomainAndResultDigestMatchIndependentEncoding() (gas: 31226)\n[PASS] test_DrawCannotBeSettledAgain() (gas: 440907)\n[PASS] test_DrawRequiresCanonicalNoWinner() (gas: 437181)\n[PASS] test_ECDSASignerWithCodeCanStillSignDirectly() (gas: 436985)\n[PASS] test_ERC1271SignerAcceptsValidSignatureAndRejectsRevocationAndReverts() (gas: 1050208)\n[PASS] test_ExpiredSignatureRejectedButExactDeadlineAccepted() (gas: 452695)\n[PASS] test_MalformedInvalidVAndHighSSignaturesRejected() (gas: 688037)\n[PASS] test_MatchIdWinnerDrawAndDeadlineAreAllBound() (gas: 1028389)\n[PASS] test_ReplayAndFreshConflictingResultCannotDoubleSettle() (gas: 486054)\n[PASS] test_SignerRotationInvalidatesOutstandingOldKeyResults() (gas: 498157)\n[PASS] test_UnavailableSignerCannotPreventTimedRefund() (gas: 792321)\n[PASS] test_WrongDomainNameVersionAndPersonalSignAreRejected() (gas: 477138)\n[PASS] test_WrongSignerIsRejected() (gas: 397663)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 61.32ms (66.24ms CPU time)\n\nRan 24 tests for test/PinballEscrow.t.sol:PinballEscrowTest\n[PASS] testFuzz_AllowedStakesConserveFunds(bool,uint8,uint8,bytes32) (runs: 512, μ: 531094, ~: 535626)\n[PASS] testFuzz_BothRefundPathsBothTokens(bool,bool,uint8) (runs: 512, μ: 517067, ~: 456230)\n[PASS] testFuzz_OnlyOwnerCanAdminister(address) (runs: 512, μ: 75987, ~: 76036)\n[PASS] testFuzz_RejectsUnsupportedTokens(address) (runs: 512, μ: 52141, ~: 52194)\n[PASS] testFuzz_RejectsWrongStake(bool,uint256) (runs: 512, μ: 76115, ~: 76067)\n[PASS] test_AllTiersBothTokensBothWinnersAndDraw() (gas: 12464797)\n[PASS] test_CannotSettleUnjoinedMatch() (gas: 237055)\n[PASS] test_ConcurrentMatchesAndDonationsCannotChangeAPot() (gas: 1284277)\n[PASS] test_ConstructorRejectsIncorrectOrZeroInitialSigner() (gas: 3901)\n[PASS] test_DeploymentOnEmptyChainHasFixedConfiguration() (gas: 332114)\n[PASS] test_JoinClosesAtExactOneHourBoundary() (gas: 294787)\n[PASS] test_JoinedRefundWorksWhilePaused() (gas: 436453)\n[PASS] test_OpenAndJoinRecordExactDepositsAndEvents() (gas: 466084)\n[PASS] test_OwnerHasNoWithdrawalOwnershipTransferOrUpgradeSurface() (gas: 517929)\n[PASS] test_PauseOnlyPreventsNewOpensAndUnpauseRestoresThem() (gas: 967352)\n[PASS] test_RejectsDuplicateOpenSelfJoinAndThirdPlayer() (gas: 462887)\n[PASS] test_RejectsMissingMatches() (gas: 101846)\n[PASS] test_RejectsNativeEtherAndUnknownCalls() (gas: 60806)\n[PASS] test_RejectsWholeTokenAmountsAndOtherTokenTiers() (gas: 145034)\n[PASS] test_RuntimeMeetsProtectedOpcodeAndSizeFloor() (gas: 3829057)\n[PASS] test_SettledIdsCannotReopenRefundOrJoin() (gas: 509808)\n[PASS] test_SettlementSucceedsOneSecondBeforeTimeout() (gas: 437419)\n[PASS] test_SettlementWindowStartsAtJoinAndClosesAtExactBoundary() (gas: 489854)\n[PASS] test_SignerAndPauseEventsAndInvalidChanges() (gas: 203800)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 64.16ms (313.60ms CPU time)\n\nRan 5 tests for test/PinballEscrowEdges.t.sol:PinballEscrowEdgesTest\n[PASS] testFuzz_ApprovalForTheOtherTokenCannotFundAJoin(bool,uint8) (runs: 512, μ: 602301, ~: 602302)\n[PASS] testFuzz_EntireBalancesAndExactAllowancesCanFundAndExit(bool,uint8,uint8,uint256) (runs: 1000, μ: 713181, ~: 743395)\nLogs:\n  Bound result 2\n  Bound result 1537\n\n[PASS] testFuzz_MalformedSignatureLengthsCannotReleaseAJoinedPot(bytes,bool) (runs: 512, μ: 500958, ~: 500710)\n[PASS] test_AllExitPaymentFailuresBothTokensAreAtomicAndRetryable() (gas: 37710151)\n[PASS] test_EveryStakeNeighbourAndIntegerExtremeIsRejectedWithoutTakingFunds() (gas: 2771030)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 65.69ms (222.09ms CPU time)\n\nRan 1 test for test/PinballEscrowInvariant.t.sol:PinballEscrowInvariantTest\n[PASS] invariant_StateAndBalancesAlwaysMatchTheIndependentModel() (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+--------------+-------+---------+----------╮\n| Contract      | Selector     | Calls | Reverts | Discards |\n+===========================================================+\n| EscrowHandler | advanceTime  | 1010  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | donate       | 977   | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | joinMatch    | 1025  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | openMatch    | 1081  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | refundMatch  | 1016  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | rotateSigner | 1044  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | settleMatch  | 1024  | 0       | 0        |\n|---------------+--------------+-------+---------+----------|\n| EscrowHandler | togglePause  | 1015  | 0       | 0        |\n╰---------------+--------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.16s (3.16s CPU time)\n\nRan 2 tests for test/PinballEscrowContinuousInvariant.t.sol:PinballEscrowContinuousInvariantTest\n[PASS] invariant_ContinuousCustodyAndRecovery() (runs: 256, calls: 32768, reverts: 0)\n\n╭-------------------------+----------------------+-------+---------+----------╮\n| Contract                | Selector             | Calls | Reverts | Discards |\n+=============================================================================+\n| ContinuousEscrowHandler | administer           | 4780  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | advanceTime          | 4670  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | donate               | 4741  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | failPaymentThenRetry | 4578  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | progress             | 4712  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | rejectDeposit        | 4689  | 0       | 0        |\n|-------------------------+----------------------+-------+---------+----------|\n| ContinuousEscrowHandler | replayFinished       | 4598  | 0       | 0        |\n╰-------------------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 999999999999500002\n  Bound result 1021\n  Bound result 91836463165229662\n  Bound result 33\n  Bound result 3292\n  Bound result 4214\n  Bound result 6747\n  Bound result 3869\n  Bound result 15247\n  Bound result 9174\n  Bound result 5950\n  Bound result 20518\n  Bound result 6126\n  Bound result 32109526949446478\n  Bound result 101647545758949105\n  Bound result 981725589384495355\n  Bound result 19348\n  Bound result 746560264808813775\n  Bound result 4047\n\n[PASS] test_HandlerExercisesFailuresFreshMatchesAndEveryExit() (gas: 11019471)\nLogs:\n  Bound result 1\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 11.90s (11.91s CPU time)\n\nRan 6 test suites in 11.90s (15.27s CPU time): 63 tests passed, 0 failed, 0 skipped (63 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PinballEscrow.join(bytes32)\",\"PinballEscrow.open(bytes32,address,uint256)\",\"PinballEscrow.pause()\",\"PinballEscrow.refund(bytes32)\",\"PinballEscrow.setSigner(address)\",\"PinballEscrow.settle(bytes32,address,bool,uint256,bytes)\",\"PinballEscrow.unpause()\"],\"files\":{\".gitignore\":4,\"LICENSE\":21,\"README.md\":132,\"docs/dependencies.json\":18,\"docs/security-review.md\":46,\"docs/validation.md\":30,\"foundry.toml\":20,\"remappings.txt\":2,\"src/PinballEscrow.sol\":213,\"test/CONTRIBUTOR-TESTS.md\":61,\"test/PinballEscrow.t.sol\":392,\"test/PinballEscrowContinuousInvariant.t.sol\":88,\"test/PinballEscrowEdges.t.sol\":175,\"test/PinballEscrowInvariant.t.sol\":291,\"test/PinballEscrowSignatures.t.sol\":239,\"test/PinballEscrowTokens.t.sol\":257,\"test/helpers/ContinuousEscrowHandler.sol\":356,\"test/helpers/EscrowTestBase.sol\":121,\"test/helpers/MockToken.sol\":92,\"test/helpers/SignerMock.sol\":29},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"c1516214714a1bc10fcdee292856a347228a0872a9eec3b6c8a81496725a8299","verifiedTreeHash":"0a860855ae4cdf91b7b6775ea904e942dd4601db","verifierVersion":"0.1.0+a6ee6548"}]}