{"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":"a82821d1-8c4e-4fc1-959d-becc7380deea","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"8bea8a976b9ddff789a541dfdaed8a421d70fe05b346a3056e4605d365a10831","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":"f7fe4208447c4942dd5bd780e29b21427b852693c9dadf232ea8f89296c2c104","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":"7a00183d46c1a500506f898194ee75777fc004dfbd1dbeffbc879ca27299c459","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":"6d328c5c0164512268b253b6a5d6c01b673fe20e946ce4f7b29190ca2554ed40","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":"0e5941a09cd9e7ccc1f358390c8c06ef3edd96e0bf73bacaae214f776f8dd0f0","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":"a18e2b0f30cb852407b923f988ddc47e4fec04bcb24e447a5eac0af1e4981791","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":"eb29997f64513f333035176d5754ca4de058cc67a5315cd425152864237e3856","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"d0142f2bb8285933c0bf9e0f6c0b7c7483b3f7d093f3da1c5b68cd612f807125","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 NightshiftVault, one contract for NIGHTSHIFT players (web and Telegram Mini App): game balances with personal deposit addresses, staked games without a signature per game, a shop for in-game items, and weekly leaderboard prizes. Player money stays here and can only go into games the player starts or back to the player's own wallet.\n\nAssets: native ETH (address(0)), IMD 0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7, SHIFT 0xe5e58e53d0bb204aa62c995a6c8cc8a48e60c3ac (ERC-20, 18 decimals, no transfer fee). Every function works for all three; ETH by call with a checked result.\n\nAccounts: bytes32 accountId with an owner wallet. register(accountId, owner, deadline, ownerSig, operatorSig): EIP-712 Register(bytes32 accountId,address owner,uint256 deadline) signed by both the owner and the operator. Only the owner changes the owner (with the new owner's signature). Events Registered, OwnerSet.\n\nDeposits: depositAddress(accountId) is a CREATE2 forwarder (salt accountId) taking ETH and tokens from any wallet or exchange. sweep(accountId, asset), callable by anyone, deploys it if needed and credits its whole balance to the account; it can only send here for its own account. Also payable depositETH(accountId) and deposit(accountId, token, amount). Event Deposited.\n\nWithdraw: withdraw(accountId, asset, amount) only by the owner, only to the owner, works while paused or frozen.\n\nGames (an internal escrow with the rules of NIGHTSHIFT's GameEscrow 0xf460fd9ad76f18414f54a2431870e8a116193489): the operator calls lockGame(gameId, accountA, accountB, asset, stake, expiresAt) at the players' request: both stakes move from available to locked; gameId new; accounts and owners distinct. settle(gameId, result, deadline, sig) with EIP-712 Result(bytes32 gameId,uint8 result,uint256 deadline) from the result signer: 0 A won, 1 B won, 2 draw or void. The winner gets 95% of the pot as available balance, 5% goes to the fee splitter 0x1119d5df2f046bc2d3c0f6279ef2e9ce49422d44 (it takes ETH and tokens); draw or void refunds both, no fee. refundGame by anyone 24 h after expiresAt.\n\nCaps against a compromised operator: per account and asset a rolling 24 h stake cap (default 0.05 ETH, 100 IMD, 2500000 SHIFT) and a per-game max; lockGame reverts above them. The owner lowers caps or freezes instantly; raising caps or unfreezing takes 24 h. Events CapsSet, Frozen.\n\nShop: buy(accountId, itemId, asset, amount) from the account's balance, signed by the owner or by the operator within the same daily cap; or payable/transferFrom buyDirect(accountId, itemId, asset, amount) by anyone. Event Purchased(accountId, itemId, asset, amount). Every purchase splits at once: IMD half to IMD's BurnExecutor 0xe29386719c155b6847ad5a4e97c6674f10ffc750, half to the prize pot; SHIFT half to 0x000000000000000000000000000000000000dEaD, half to the pot; ETH half to an ETH burn reserve, half to the pot. burnReserve(amountIn, minShiftOut), by the keeper only: buys SHIFT with that ETH on the Uniswap v4 SHIFT/ETH pool (key: currency0 ETH, currency1 SHIFT, fee 12500, tickSpacing 60, hooks 0x42ebcf86e3ab0445ba0362f911a7e8fad6ace0cc) and sends it all to 0x…dEaD in the same call, requiring minShiftOut. Events Burned.\n\nWeekly prizes: the pot is tracked per asset. postPrizes(epoch, root, asset, total) by the operator reserves total from that asset's pot (never more than it holds), once per epoch and asset. claimPrize(epoch, accountId, asset, amount, proof) credits the account's available balance; OpenZeppelin StandardMerkleTree leaf keccak256(bytes.concat(keccak256(abi.encode(epoch, accountId, asset, amount)))); once per leaf; unclaimed after 30 days returns to the pot with expirePrizes. Events PrizesPosted, PrizeClaimed.\n\nRoles: operator, result signer and keeper are set by the contract owner 0x6c4c78f2c3bdf4c4318fe305fb1c8bc1694d19b5 (initially 0x3b5919586fc04f7e99b450a72bcb14eeb6977497 for all three); each change takes effect after 48 h. The contract owner may pause new games, deposits and purchases; nothing else. Nobody can move, take or redirect a player balance, the pot or the burn reserve except by the rules above, or change the split, the fee, the recipients or the assets. No upgradeability, delegatecall, selfdestruct or sweeping. Reentrancy guard, checks-effects-interactions, SafeERC20, Solidity 0.8.x, NatSpec, events everywhere. EIP-712 domain \"NightshiftVault\", version \"1\".\n\nViews: accountOf, balanceOf(accountId, asset) available and locked, depositAddress, games, pot(asset), burnReserve, epochs.\n\nFoundry tests for ETH, IMD and SHIFT: registration needs both signatures; transfer then sweep; withdraw only to the owner, paused and frozen; caps, delays and freeze; settle A, B, draw, wrong signer, expired, double; refund; 5% fee; every purchase split exactly half and half; burnReserve with a mocked pool and minShiftOut; prize posting within the pot, claims, double claims, expiry; role delays; owner cannot move funds; invariants that balances plus locked plus pot plus reserve never exceed holdings.","parentJobId":null,"planHash":"b182948b9abb4258cfd9ad0c606b8d322beea9eb460885a33beb9ad10f3aa075","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"a82821d1-8c4e-4fc1-959d-becc7380deea","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2329-build-deploy-nightshiftvault-one"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"54230","feedbackHash":"cb1d4135d3f86e8a67cd8bf6d8cfed05a8257454e6b5b9264348a98f9d860396","nodeKey":"audit_economics","submissionHash":"8bea8a976b9ddff789a541dfdaed8a421d70fe05b346a3056e4605d365a10831","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52258","feedbackHash":"bf6c9b802c9141a8abe1de7c8a220f26ebbb4868a961b1169d11aab29874257a","nodeKey":"audit_flow","submissionHash":"f7fe4208447c4942dd5bd780e29b21427b852693c9dadf232ea8f89296c2c104","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51347","feedbackHash":"3f3e15b861ce3e2096f28bd0f9f6eb1711e33670ee949d1cb5cfec2379e8af42","nodeKey":"audit_judge","submissionHash":"7a00183d46c1a500506f898194ee75777fc004dfbd1dbeffbc879ca27299c459","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"54229","feedbackHash":"62b3850b6851e9ab0c75f25014185a5cf837dd5bf5438a252946d9192ed050cb","nodeKey":"audit_math","submissionHash":"6d328c5c0164512268b253b6a5d6c01b673fe20e946ce4f7b29190ca2554ed40","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50989","feedbackHash":"de9e6a95e6ab9c05e0f1ff5dc8b31c36fa85b7b56bb09b06ca00d0538a3dbafc","nodeKey":"audit_permissions","submissionHash":"0e5941a09cd9e7ccc1f358390c8c06ef3edd96e0bf73bacaae214f776f8dd0f0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52691","feedbackHash":"741da17fbfd9b0785bcc0ceae1be05314dd7262ea31991101488cfac87329be5","nodeKey":"build_contract_project","submissionHash":"a18e2b0f30cb852407b923f988ddc47e4fec04bcb24e447a5eac0af1e4981791","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51139","feedbackHash":"5bb74a16fc1fc7badd049ee7fae7e0cd2020a108ac8ed23708a962c6a9c17ae8","nodeKey":"manifest","submissionHash":"eb29997f64513f333035176d5754ca4de058cc67a5315cd425152864237e3856","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51466","feedbackHash":"9bb1ea8c395a58573ae65e587def569869b16c3b86549ba46f157445eb2da745","nodeKey":"write_foundry_tests","submissionHash":"d0142f2bb8285933c0bf9e0f6c0b7c7483b3f7d093f3da1c5b68cd612f807125","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"64b131ce51f8af05ad49e3b0114535c15164aa244fe237338e3729905de5ca9c","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e36579e0223ff908","findings":[{"citation":"resolved","description":"Trust gap (access x economics). The brief frames the per-account caps as the bound on what a compromised operator can take. That bound only covers balances already credited to a registered account. depositAddress(accountId) is deterministic and accepts ETH/IMD/SHIFT before any registration, and register() lets the operator pick the owner for a not-yet-registered accountId as long as that owner co-signs. Nothing ties an accountId to a player before registration, so whatever sits at the forwarder is credited to whichever owner the operator registers first. Because register() is first-come (AlreadyRegistered), a compromised operator can front-run the player's own registration transaction for the same accountId and then withdraw everything via sweep + withdraw, with no cap, freeze or delay in the way. The NatSpec on depositAddress tells integrators to register before funding, but the Telegram/web flow shows a deposit address derived from the service-assigned accountId, and exchange withdrawals routinely land before a registration is mined. Suggested mitigations that keep the design: bind the Register digest to the forwarder funding (e.g. require the owner signature to be the one the player made when the address was issued, or let sweep() for a not-yet-registered id revert until the id has been registered for at least one cap delay), or at minimum state in the app that nothing may be sent to the address before Registered is emitted.","line":285,"path":"src/NightshiftVault.sol","reproduction":"State: accountId A unregistered; an exchange sends 1 ETH to vault.depositAddress(A). Player Alice's register(A, alice, deadline, aliceSig, opSig) is pending. Operator key holder Mal submits register(A, mal, deadline, malSig, opSig) first: succeeds (both signatures valid, accountOf[A]==0). Alice's tx then reverts AlreadyRegistered. Anyone calls sweep(A, address(0)) -> credits 1 ETH to A; Mal calls withdraw(A, address(0), 1 ether) -> receives 1 ETH. Expected: a compromised operator is limited to the 0.05 ETH daily cap; actual: the full pre-registration balance is lost. Verified with test/scratch/Probe.t.sol test_preRegistrationForwarderFundsFollowOperatorChosenOwner (passes on current code, demonstrating the path).","severity":"low","snippet":"        _signature(operator(), digest, operatorSig);","title":"Funds parked at an unregistered deposit address are outside the operator caps: a compromised operator registers the accountId to its own wallet and takes the whole balance"},{"citation":"resolved","description":"Asymmetry between the setter and the consumer of purchaseNonces. The owner may set the nonce to any value strictly above the current one, including type(uint256).max, but buyWithSig() always executes ++purchaseNonces[accountId] after verifying the signature, which reverts with an arithmetic panic once the stored nonce is max. The setter rejects any later value that is not strictly greater, so there is no way back: every future relayed purchase (owner-signed or operator-signed) for that account reverts forever, even though the signature is valid and the deadline unexpired. Direct buy() by the owner or operator still works, so no funds are lost, but the relayed purchase path that the Telegram Mini App relies on (players without gas) is permanently broken for that account and the owner has no on-chain remedy except transferring ownership, which does not reset purchaseNonces either. A bounded increment (e.g. require newNonce <= current + some limit, or newNonce < type(uint256).max) or using a per-account bitmap/cancellation flag instead of a free-form nonce avoids the dead end.","line":555,"path":"src/NightshiftVault.sol","reproduction":"Registered account A owned by alice with 1 ETH available. alice calls invalidatePurchaseNonce(A, type(uint256).max): succeeds. alice calls invalidatePurchaseNonce(A, type(uint256).max) again: reverts InvalidAmount (nothing higher exists). alice signs Purchase(A, item, ETH, 2 wei, alice, nonce=max, ownerNonce=0, deadline) and anyone calls buyWithSig(A, item, address(0), 2, alice, deadline, sig): expected Purchased event; actual revert with panic 0x11 at ++purchaseNonces[accountId]. Verified with test/scratch/Probe.t.sol test_nonceMaxBricksBuyWithSig.","severity":"low","snippet":"        if (newNonce <= purchaseNonces[accountId]) revert InvalidAmount();\n        purchaseNonces[accountId] = newNonce;","title":"invalidatePurchaseNonce(type(uint256).max) irreversibly disables buyWithSig for the account because ++purchaseNonces overflows"},{"citation":"resolved","description":"Not a code defect; recorded as the material privileged power in the reviewed area, as the Trust Gap guide asks. The brief bounds a compromised operator by per-account caps and says nobody can take the pot except by the rules above, but the rules let the operator post any Merkle root for any epoch number with total up to the full pot[asset]. No delay, no owner approval and no relation to leaderboard data is checked on-chain. A compromised operator therefore converts the entire pot (half of every purchase ever made, plus every expired prize) into its own available balance in one transaction pair and withdraws it. This is consistent with the requested design, so it is reported as a trust assumption for the launch record rather than a finding; a 24-48 h delay on postPrizes (mirroring the cap/role delays) would let the contract owner pause or rotate the operator before a bad root becomes claimable, without changing who may post.","line":636,"path":"src/NightshiftVault.sol","reproduction":"pot[ETH] = 10 ETH after purchases. Compromised operator holds a registered account X. Operator calls postPrizes(999, root, address(0), 10 ether) where root is the single-leaf tree keccak256(bytes.concat(keccak256(abi.encode(999, X, address(0), 10 ether)))). Then claimPrize(999, X, address(0), 10 ether, []) succeeds (leaf == root, remaining == total) and withdraw(X, address(0), 10 ether) sends the whole pot to the operator's wallet in the same block. Caps, freeze and pause do not apply to any of these three calls.","severity":"info","snippet":"        if (root == bytes32(0) || epochs[epoch][asset].expiresAt != 0) revert InvalidEpoch();\n        if (total == 0 || total > pot[asset]) revert InvalidAmount();","title":"Trust assumption: the operator alone decides who receives the whole prize pot (postPrizes root is unconstrained), so the pot is not covered by the per-account caps"}],"hash":"0e5941a09cd9e7ccc1f358390c8c06ef3edd96e0bf73bacaae214f776f8dd0f0","nodeId":"901f6781-5678-4640-afb5-0555d4a36761","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the result: two low findings, one info trust assumption, and a coverage row for all 27 entry points plus two invariants.\n\n**What I found in the assigned area (Access Control, Trust Gap, Asymmetry)**\n\n- **Low, register at `src/NightshiftVault.sol:285`.** Funds sent to a deposit address before that accountId is registered are outside every operator bound. A compromised operator co-signs a registration naming its own wallet, front-runs the player's pending registration for the same accountId, then sweeps and withdraws the whole balance. The daily cap only covers balances already credited to a registered account. Confirmed with a scratch test that takes 1 ETH against a 0.05 ETH cap.\n- **Low, invalidatePurchaseNonce at `src/NightshiftVault.sol:555`.** The owner may set the purchase nonce to the maximum uint256, after which every relayed purchase reverts on the increment and the setter refuses any new value. The gasless purchase path is permanently dead for that account. Direct purchases still work, so no funds are lost.\n- **Info, postPrizes at `src/NightshiftVault.sol:636`.** Recorded as the main trust assumption rather than a defect: the operator can post any root for the full pot and claim it to its own account in the same block. This matches the brief, so it is documented for the launch record with a suggested delay.\n\n**What held.** Every other entry point traced clean for my area: the owner has only pause and 48-hour role rotation, every fund recipient is a constant, withdraw goes only to the account owner, caps and freeze cannot be accelerated, settle and refund are mutually exclusive, and the pool callback is bound to one in-flight keeper burn. I verified on-chain that the fee splitter accepts plain ETH, so settlement with a winner cannot be blocked by it. The existing suite of 56 tests passes.\n\n**Not fully reached.** I did not verify the live SHIFT hook's trading-open flag or hook fee behaviour against the burn path. That sits outside my area and affects only whether burns revert, not custody.\n\n**Note on the static analysis leads.** The slither and aderyn lines for sweep reentrancy, encodePacked collision, uninitialized `_spending` and timestamp use were each traced and none produced a reachable defect.","treeHash":null,"usage":{"cachedInputTokens":1912465,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":38313,"runtime":"claude","turns":29,"wallClockMs":626336}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0ef0dfae5a05e4be","findings":[{"citation":"resolved","description":"Boundary x invariant seam. invalidatePurchaseNonce only requires newNonce > purchaseNonces[accountId] (line 555) and has no upper bound, so the owner can set the nonce to type(uint256).max. buyWithSig then always executes ++purchaseNonces[accountId] under checked arithmetic, which panics (0x11) before _buy runs. There is no function that lowers the nonce, so the relayed-signature purchase path for that account is dead forever: the owner cannot later authorize gasless purchases and the operator cannot use operator-signed purchases for it either. buy() and buyDirect() still work, and no funds are lost, hence low. Fix: in invalidatePurchaseNonce require newNonce < type(uint256).max (or cap the jump), or in buyWithSig use unchecked increment semantics only after validating the nonce, matching the pattern the owner-nonce path uses.","line":548,"path":"src/NightshiftVault.sol","reproduction":"State: account A registered to alice, 10 IMD deposited. 1) alice calls invalidatePurchaseNonce(A, 2**256-1): passes, purchaseNonces[A] = 2**256-1. 2) alice signs Purchase(A, ITEM, IMD, 2e18, alice, nonce=2**256-1, ownerNonce=0, deadline=now+1h) and anyone calls buyWithSig(A, ITEM, IMD, 2e18, alice, deadline, sig). Expected: purchase executes (valid owner signature, correct nonce). Actual: reverts with arithmetic overflow panic at ++purchaseNonces[accountId]; every future buyWithSig for A reverts the same way with no recovery. Verified with test/scratch/Edges.t.sol::test_maxPurchaseNonceBricksBuyWithSig (buy() afterwards still succeeds and leaves 8 IMD available).","severity":"low","snippet":"        ++purchaseNonces[accountId];","title":"invalidatePurchaseNonce(type(uint256).max) permanently bricks buyWithSig for the account (checked ++ overflows)"},{"citation":"resolved","description":"Precision: fee = (2*stake)/20 rounds down, so for stake < 10 wei the fee is 0 and for larger stakes the fee is short by up to 1 wei (e.g. stake 15 wei: pot 30, fee 1 = 3.3%). Conservation holds (totalPlayerFunds -= fee, winner += pot - fee), the loss is bounded by 1 wei per game and cannot be farmed for value with 18-decimal assets, so this is informational. If exact 5% matters, round the fee up (fee = (gamePot + 19) / 20) or require stake to be a multiple of 10 in lockGame.","line":497,"path":"src/NightshiftVault.sol","reproduction":"A and B each deposit 9 wei IMD; operator lockGame(id, A, B, IMD, 9, now+1h); settle(id, 0, ...) with a valid result signature. Expected: fee = 5% of 18 wei pot (~0.9 wei, i.e. at least 1 wei if rounding up). Actual: fee = 18/20 = 0, A's available becomes 18, totalPlayerFunds unchanged, nothing sent to FEE_SPLITTER. Verified with test/scratch/Edges.t.sol::test_feeTruncatesToZeroForTinyStakes.","severity":"info","snippet":"            fee = gamePot / 20;","title":"5% settlement fee truncates to zero for pots under 20 wei (winner keeps 100%)"},{"citation":"resolved","description":"Zero-input boundary: when the forwarder holds nothing, DepositForwarder.sweep returns 0, amount == sent == 0 passes the mismatch check, and _creditDeposit emits Deposited(accountId, asset, forwarder, 0). Anyone can call sweep for any registered account, so an indexer that treats every Deposited event as a real deposit can be spammed with zero-value events at the caller's gas cost. No accounting effect. Fix: return early (or revert InvalidAmount) when amount == 0 before crediting, keeping ForwarderDeployed on first call.","line":341,"path":"src/NightshiftVault.sol","reproduction":"Account A registered, nothing ever sent to depositAddress(A). Any caller runs sweep(A, address(0)). Expected: no Deposited event (nothing was deposited). Actual: call succeeds, returns 0 and emits Deposited(A, 0x0, depositAddress(A), 0). Verified with test/scratch/Edges.t.sol::test_sweepEmptyForwarderEmitsZeroDeposit.","severity":"info","snippet":"        _creditDeposit(accountId, asset, amount, forwarder);","title":"sweep on an empty forwarder succeeds and emits Deposited with amount 0"},{"citation":"resolved","description":"Boundary note, not a defect: to make the 50/50 split exact without a dust remainder, buy, buyWithSig and buyDirect all reject odd wei amounts. The brief says every purchase splits half and half; this is the implementation's way of guaranteeing it. The game service and any price list must only quote even raw amounts, otherwise valid-looking purchases revert with InvalidAmount. Documented here so the author's off-chain pricing honours the constraint.","line":783,"path":"src/NightshiftVault.sol","reproduction":"alice (owner of A, 10 IMD available) calls buy(A, ITEM, IMD, 1e18 + 1). Expected per a naive reading of the brief: purchase for 1000000000000000001 wei splits. Actual: reverts InvalidAmount at _purchaseAmount. Same for buyDirect{value: 3}(A, ITEM, address(0), 3).","severity":"info","snippet":"        if (amount == 0 || amount % 2 != 0) revert InvalidAmount();","title":"Purchases with odd raw amounts are rejected (even-only amounts required for the half/half split)"}],"hash":"6d328c5c0164512268b253b6a5d6c01b673fe20e946ce4f7b29190ca2554ed40","nodeId":"fc93dd6e-d45f-4bec-aeae-cc120cd0de6a","outcome":"completed","summary":"Review complete. The findings file is written and validated, and no source file was changed.\n\n**Outcome.** The math in NightshiftVault holds up under the Math Precision, Boundary and Numerical Gap checklists. I found no critical, high or medium defect in that area, so no proof tests were attached. The report in `.imd-findings.json` carries one low finding, three informational notes and a coverage row for every one of the 27 entry points plus three invariants.\n\n**Findings recorded:**\n- **Low.** The owner can set the purchase nonce to the maximum value with no upper bound, after which every relayed purchase for that account panics on the checked increment forever. Direct purchases still work, so no funds are at risk.\n- **Info.** The 5% settlement fee rounds down, which means zero fee for pots under 20 wei and at most 1 wei short otherwise. Conservation still holds.\n- **Info.** Sweeping an empty forwarder succeeds and emits a zero-amount Deposited event that anyone can trigger.\n- **Info.** All purchase paths reject odd raw amounts to keep the half-and-half split exact, so off-chain prices must be even.\n\n**What I verified beyond reading.** The project's 56 tests pass, including the invariant suite. Four scratch tests in the gitignored scratch directory confirmed each reported edge. Against live mainnet state, I overlaid the vault bytecode with a funded reserve and simulated a real burn of 0.001 ETH through the production pool and hook, which returned about 37,464 SHIFT, so the swap, delta unpacking and hook interaction work in production. I also confirmed the fee splitter accepts a plain ETH push, the pool key is initialized with the expected fee, and IMD and SHIFT transfers to their burn destinations return true from a live holder.\n\n**Coverage.** Every entry point is answered as holds or finding. No row is unreached. Areas I touched only lightly because they belong to other specialists: the operator trust model around registering accounts for pre-funded forwarder addresses, and the hook's ability to take output fees, which the keeper's minimum-output parameter already bounds.","treeHash":null,"usage":{"cachedInputTokens":1602423,"inputTokens":484,"model":"claude-fable-5-1","outputTokens":41719,"runtime":"claude","turns":34,"wallClockMs":1227555}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"af9a875696459139","findings":[{"citation":"resolved","description":"Merged from audit_permissions and audit_economics (same root cause). depositAddress(accountId) is a pure function of the id and accepts ETH/IMD/SHIFT before any registration, while register() binds an unused id to whichever owner the operator co-signs, first come first served (AlreadyRegistered for everyone after). The operator key can sign both halves for its own wallet, so whoever holds it can front-run a player's pending register(accountId, player, ...) for an id that already holds funds, then call sweep (permissionless) and withdraw as the account owner. None of the cap, freeze, delay or pause checks sit on that path, so the amount taken is bounded only by what was parked, not by the 0.05 ETH / 100 IMD / 2.5M SHIFT daily caps the brief relies on against a compromised operator. The NatSpec on depositAddress says to register before funding, but nothing enforces it on chain and exchange withdrawals to a displayed address routinely land before a registration is mined. Preconditions: operator key compromise and a deposit that reaches the forwarder before registration. Suggested minimal fix that keeps the design: derive the required accountId from the owner (e.g. accountId == keccak256(abi.encode(owner, salt)) checked in register) so a forwarder can only ever be credited to the wallet it was generated for, or have the client reveal depositAddress only after Registered is observed and state that constraint in the brief.","line":286,"path":"src/NightshiftVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftVault} from \"src/NightshiftVault.sol\";\n\n/// @dev Funds parked at depositAddress(id) before register(id, ...) lands are outside the cap model:\n///      the operator alone can register that id to its own wallet, sweep and withdraw everything.\ncontract PreRegistrationForwarderTest is Test {\n    NightshiftVault vault;\n    uint256 constant OP_KEY = 0x0a011;\n    uint256 constant USER_KEY = 0xa11ce;\n    address op;\n    address user;\n    bytes32 constant ID = keccak256(\"new player\");\n\n    function setUp() public {\n        vm.warp(10 days);\n        op = vm.addr(OP_KEY);\n        user = vm.addr(USER_KEY);\n        vault = new NightshiftVault();\n        vm.prank(vault.OWNER());\n        vault.scheduleRoleChange(NightshiftVault.Role.Operator, op);\n        vm.warp(vm.getBlockTimestamp() + 48 hours);\n    }\n\n    function _sig(uint256 key, bytes32 digest) internal pure returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, digest);\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _digest(bytes32 id, address owner, uint256 deadline) internal view returns (bytes32) {\n        return keccak256(\n            abi.encodePacked(\n                \"\\x19\\x01\",\n                vault.DOMAIN_SEPARATOR(),\n                keccak256(abi.encode(vault.REGISTER_TYPEHASH(), id, owner, deadline))\n            )\n        );\n    }\n\n    function test_operatorTakesPreRegistrationDepositUnbounded() public {\n        // Player funds the counterfactual address with 1 ETH (20x the 0.05 ETH daily cap).\n        address forwarder = vault.depositAddress(ID);\n        vm.deal(forwarder, 1 ether);\n\n        // The operator registers the same id to its own wallet: it signs both messages itself.\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        bytes32 digest = _digest(ID, op, deadline);\n        vault.register(ID, op, deadline, _sig(OP_KEY, digest), _sig(OP_KEY, digest));\n\n        // The player's own registration can no longer land.\n        bytes32 userDigest = _digest(ID, user, deadline);\n        vm.expectRevert(NightshiftVault.AlreadyRegistered.selector);\n        vault.register(ID, user, deadline, _sig(USER_KEY, userDigest), _sig(OP_KEY, userDigest));\n\n        vault.sweep(ID, address(0));\n        vm.prank(op);\n        vault.withdraw(ID, address(0), 1 ether);\n\n        // Expected under the cap model: at most DEFAULT_ETH_CAP (0.05 ETH) per 24 h leaves the player.\n        // Actual: the whole 1 ETH is in the operator's wallet in one transaction sequence.\n        assertLe(op.balance, vault.DEFAULT_ETH_CAP(), \"operator extracted more than the daily cap\");\n    }\n}","reproduction":"Fresh vault, operator role = op (after the 48 h delay), ID = keccak256('new player') unregistered. 1) vm.deal(vault.depositAddress(ID), 1 ether). 2) op calls register(ID, op, deadline, sig(op over Register(ID, op, deadline)), same sig) -> succeeds. 3) The player's register(ID, user, deadline, userSig, opSig) -> reverts AlreadyRegistered. 4) anyone calls sweep(ID, address(0)) -> credits 1 ETH to ID. 5) op calls withdraw(ID, address(0), 1 ether) -> op.balance == 1 ether. Expected under the brief: a compromised operator moves at most DEFAULT_ETH_CAP = 0.05 ETH per 24 h; actual: 1 ETH in one sequence. Run: forge test --match-path test/scratch/PreRegistrationForwarder.t.sol -> FAIL 'operator extracted more than the daily cap: 1000000000000000000 > 50000000000000000' (I re-ran the specialist's proof on this tree).","severity":"low","snippet":"        accountOf[accountId] = accountOwner;","title":"Funds sent to a deposit address before registration are outside the compromised-operator cap model: the operator can bind the id to itself, sweep and withdraw everything"},{"citation":"resolved","description":"The vendored OpenZeppelin 5.1.0 SignatureChecker.isValidSignatureNow checks signer.code.length first and, when code is present, only tries ERC-1271; it never falls back to ECDSA. Since EIP-7702 any EOA can carry a delegation designator (0xef0100 || delegate), and delegates that do not implement isValidSignature make every Register, SetOwner (as new owner) and Purchase signature from that wallet fail with InvalidSignature although the wallet's ECDSA signature is valid. I hit this on a mainnet fork: the well-known test address vm.addr(0xa11ce) already carries delegation code on chain and register() reverted on a correct signature. Withdrawals are msg.sender based and unaffected, so no funds are lost, but such players cannot onboard and an existing owner cannot hand the account to such a wallet. Fix: in _signature try ECDSA.tryRecover first and fall back to isValidERC1271SignatureNow only when recovery does not match (the pre-5.1 OpenZeppelin behaviour), which is equally safe for EOAs.","line":711,"path":"src/NightshiftVault.sol","reproduction":"Registered operator op; dan = vm.addr(0xd00d), D = keccak256('dan'); dan signs Register(D, dan, deadline) and op co-signs. vm.etch(dan, hex'ef0100...dead') to model a delegated EOA. register(D, dan, deadline, danSig, opSig) -> reverts InvalidSignature. vm.etch(dan, '') and the identical call succeeds and accountOf(D) == dan. Verified with test/scratch/Delegated.t.sol::test_eoaWithCodeCannotRegisterWithValidEcdsa on this tree; also observed live on a mainnet fork (block 26235788) where vault.register for 0xe05fcC23807536bEe418f142D19fa0d21BB0cfF7 reverted InvalidSignature after a staticcall to that address's isValidSignature reverted.","severity":"low","snippet":"        if (signer == address(0) || !SignatureChecker.isValidSignatureNow(signer, digest, sig)) {","title":"Signers whose address carries code (EIP-7702 delegated EOAs without ERC-1271) cannot register, change owner or relay purchases even with a valid ECDSA signature"},{"citation":"resolved","description":"Merged from audit_math and audit_permissions (same root cause). invalidatePurchaseNonce only requires newNonce > current with no upper bound, while buyWithSig always executes ++purchaseNonces[accountId] under checked arithmetic after verifying the signature. Once the stored nonce is type(uint256).max every relayed purchase (owner- or operator-signed) panics with 0x11, and because the setter rejects any value that is not strictly greater there is no recovery; setOwner does not reset purchaseNonces either. buy() and buyDirect() still work and no funds are lost, so the impact is a permanently dead gasless-purchase path for that account. Fix: require newNonce < type(uint256).max in invalidatePurchaseNonce (or bound the jump).","line":555,"path":"src/NightshiftVault.sol","reproduction":"Account A owned by alice with 10 IMD available. alice calls invalidatePurchaseNonce(A, 2**256-1): succeeds. alice signs Purchase(A, ITEM, IMD, 2e18, alice, nonce=2**256-1, ownerNonce=0, deadline) and anyone calls buyWithSig(A, ITEM, IMD, 2e18, alice, deadline, sig). Expected: purchase executes. Actual: arithmetic overflow panic at ++purchaseNonces[accountId]; a second invalidatePurchaseNonce(A, 2**256-1) reverts InvalidAmount. buy(A, ITEM, IMD, 2e18) by alice afterwards still succeeds (8 IMD left). Verified with test/scratch/Leads.t.sol::test_nonceMaxBricksBuyWithSig on this tree.","severity":"low","snippet":"        if (newNonce <= purchaseNonces[accountId]) revert InvalidAmount();","title":"invalidatePurchaseNonce(type(uint256).max) permanently disables buyWithSig for the account (checked ++ overflows, no way back)"},{"citation":"resolved","description":"From audit_flow. buyWithSig binds every Purchase signature to purchaseNonces[accountId] and increments it on use regardless of whether the signer was the account owner or the operator, so the two signers compete for the same sequence number. A relayed operator-signed purchase consumes the nonce an owner's outstanding signature was built on and that signature can never be used; the owner must re-sign. The operator is the party the caps are meant to bound and it can here cancel any owner-authorised relayed purchase at will. Liveness/griefing only; no funds move wrongly. Fix: track nonces per (accountId, signer), or include a signer-specific nonce in the digest.","line":548,"path":"src/NightshiftVault.sol","reproduction":"Account A owned by alice, 10 IMD available. 1) alice signs Purchase(A, ITEM, IMD, 4e18, alice, nonce=0, ownerNonce=0, deadline). 2) op signs Purchase(A, ITEM, IMD, 2e18, op, nonce=0, ownerNonce=0, deadline) and anyone calls buyWithSig(A, ITEM, IMD, 2e18, op, deadline, opSig): succeeds, purchaseNonces[A] == 1. 3) buyWithSig(A, ITEM, IMD, 4e18, alice, deadline, aliceSig) -> reverts InvalidSignature. Expected: an owner-authorised purchase is not cancellable by the operator. Verified with test/scratch/Leads.t.sol::test_operatorConsumesOwnerPurchaseNonce on this tree.","severity":"low","snippet":"        ++purchaseNonces[accountId];","title":"Owner and operator purchase signatures share one nonce, so an operator-signed relay invalidates an outstanding owner-signed purchase (and vice versa)"},{"citation":"resolved","description":"From audit_flow. Every setCaps call recomputes readyAt from block.timestamp against the currently effective caps (which exclude a not-yet-matured raise). An owner with a pending daily raise who calls setCaps again only to tighten perGame (passing the same, still-pending daily value) restarts the daily raise's 24 h clock. The brief says lowering is instant and raising takes 24 h; here an instant lowering costs the owner up to another 24 h on an unrelated raise. Liveness/UX only, caps stay conservative. Fix: keep the existing readyAt when the new pending values are <= the pending values already scheduled, or track readyAt per cap.","line":403,"path":"src/NightshiftVault.sol","reproduction":"Account A, default ETH caps (daily 0.05, perGame 0.01). t0: alice calls setCaps(A, ETH, 1 ether, 0.01 ether) -> pending daily 1 ETH, readyAt t0+24h. t0+23h: alice calls setCaps(A, ETH, 1 ether, 0.005 ether). t0+24h+1s: caps(A, ETH) returns daily 0.05 ETH, perGame 0.005; only at t0+47h does daily become 1 ETH. Expected: daily 1 ETH at t0+24h since that raise was scheduled 24 h earlier. Verified with test/scratch/Leads.t.sol::test_setCapsLowerOneResetsPendingRaiseOfOther on this tree.","severity":"low","snippet":"        uint256 readyAt = daily > immediateDaily || perGame > immediateGame ? block.timestamp + CAP_DELAY : 0;","title":"setCaps replaces the whole pending change, so lowering one cap restarts the 24 h delay of the other cap's already-scheduled raise"},{"citation":"resolved","description":"Merged from audit_math and audit_flow. sweep does not short-circuit when the forwarder holds nothing: DepositForwarder.sweep returns 0, amount == sent == 0 passes the mismatch check and _creditDeposit emits Deposited(accountId, asset, forwarder, 0) (the forwarder also emits Forwarded with 0). Anyone can emit phantom zero-value deposits for any registered account at their own gas cost. Fix: return early (or revert InvalidAmount) when amount == 0, keeping ForwarderDeployed on first call.","line":341,"path":"src/NightshiftVault.sol","reproduction":"Account A registered, nothing sent to depositAddress(A). Anyone calls sweep(A, address(0)). Actual: returns 0 and emits Deposited(A, 0x0, depositAddress(A), 0). Expected: no Deposited event for a zero credit. Verified with test/scratch/Leads.t.sol::test_emptySweepEmitsZeroDeposit (expectEmit of the zero event passes) on this tree.","severity":"info","snippet":"        _creditDeposit(accountId, asset, amount, forwarder);","title":"sweep on an empty forwarder succeeds and emits Deposited with amount 0 (indexer spam, no accounting effect)"},{"citation":"resolved","description":"From audit_math. fee = (2*stake)/20 truncates, so for stake < 10 wei the fee is 0 and for larger stakes it is short by up to 1 wei, with the dust going to the winner. Conservation holds (totalPlayerFunds -= fee, winner += pot - fee) and the loss is bounded by 1 wei per game, so informational. If an exact 5% matters, round the fee up or require stakes to be multiples of 10.","line":497,"path":"src/NightshiftVault.sol","reproduction":"A and B each deposit 9 wei IMD; operator lockGame(id, A, B, IMD, 9, now+1h); settle(id, 0, deadline, validResultSig). Expected: fee of 5% of the 18 wei pot (at least 1 wei if rounded up). Actual: fee == 0, A's available becomes 18 wei, nothing sent to FEE_SPLITTER. Verified with test/scratch/Leads.t.sol::test_feeTruncatesToZeroForTinyStakes on this tree.","severity":"info","snippet":"            fee = gamePot / 20;","title":"5% settlement fee rounds down: zero fee for pots under 20 wei and up to 1 wei short otherwise"},{"citation":"resolved","description":"From audit_math; a documented design choice rather than a defect. buy, buyWithSig and buyDirect all reject odd wei amounts so that the brief's 'exactly half and half' split never leaves dust. The game service and any price list must quote even raw amounts or valid-looking purchases revert with InvalidAmount.","line":783,"path":"src/NightshiftVault.sol","reproduction":"alice (owner of A, 10 IMD available) calls buy(A, ITEM, IMD, 1e18 + 1) -> reverts InvalidAmount; buyDirect{value: 3}(A, ITEM, address(0), 3) -> reverts InvalidAmount. Verified with test/scratch/Leads.t.sol::test_oddAmountRejected on this tree.","severity":"info","snippet":"        if (amount == 0 || amount % 2 != 0) revert InvalidAmount();","title":"Purchases with odd raw amounts are rejected so the half/half split is exact; off-chain prices must be even"},{"citation":"resolved","description":"From audit_flow. settle sends the fee in the same transaction that credits the winner. For ETH games a revert in the fee splitter's receive path reverts settlement; the only exit left is refundGame after expiresAt + 24 h, which returns both stakes and discards the signed result, so the winner loses their winnings. The live splitter 0x1119d5Df2f046bC2d3c0F6279EF2E9cE49422d44 has code (15,083 bytes) and the flow specialist observed it accepting a plain ETH call, so this is a dependency on an owned external contract rather than a present failure. The brief asks for push to the fee splitter, so recorded for awareness; a pull-based fee accrual would remove the dependency.","line":502,"path":"src/NightshiftVault.sol","reproduction":"ETH game between A and B (0.01 ETH each). vm.etch a contract whose receive() reverts at FEE_SPLITTER, then settle(gameId, 0, deadline, validResultSig) -> reverts ETHTransferFailed; after expiresAt + 24 h refundGame(gameId) returns both stakes (A available 0.01 ETH). Expected per design: winner credited 95%. Verified with test/scratch/Leads.t.sol::test_feeSplitterRejectBlocksSettlement on this tree.","severity":"info","snippet":"        if (fee != 0) _send(game.asset, FEE_SPLITTER, fee);","title":"Trust assumption: settlement pushes the 5% fee to FEE_SPLITTER in the same call; if that external contract ever rejects ETH, winners cannot be paid and only a refund remains"},{"citation":"resolved","description":"From audit_permissions; consistent with the requested design, recorded as the material privileged power. The brief bounds a compromised operator by per-account caps, but the operator may post any Merkle root for any epoch with total up to the full pot[asset], with no delay, owner approval or relation to leaderboard data checked on chain. A compromised operator therefore converts the entire pot (half of every purchase plus expired prizes) into its own available balance and withdraws it in one transaction pair; pause, caps and freeze do not apply to postPrizes, claimPrize or withdraw. A 24-48 h delay on postPrizes (mirroring cap/role delays) would let the contract owner rotate the operator before a bad root becomes claimable without changing who may post.","line":637,"path":"src/NightshiftVault.sol","reproduction":"buyDirect 10 ETH for A -> pot[ETH] == 5 ETH. Operator registers account X to its own wallet. postPrizes(999, leaf, address(0), 5 ether) with leaf = keccak256(bytes.concat(keccak256(abi.encode(999, X, address(0), 5 ether)))) succeeds; claimPrize(999, X, address(0), 5 ether, []) succeeds (single-leaf tree, leaf == root); withdraw(X, address(0), 5 ether) by op -> op.balance == 5 ether. Verified with test/scratch/Leads.t.sol::test_operatorPostsWholePotToItself on this tree.","severity":"info","snippet":"        if (total == 0 || total > pot[asset]) revert InvalidAmount();","title":"Trust assumption: the operator alone decides who receives the whole prize pot (postPrizes root is unconstrained), so the pot is not covered by the per-account caps"}],"hash":"7a00183d46c1a500506f898194ee75777fc004dfbd1dbeffbc879ca27299c459","nodeId":"a63ed168-c8fb-45ae-a5e5-18343ea24bfc","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds 10 substantiated findings plus a 29-row coverage record answering all 27 entry points.\n\n**Outcome.** No critical or high defects. The contract matches the brief on every rule I traced: custody, caps, delays, settlement, splits, prizes, roles and pause scope. All 77 project tests pass, and I ran burnReserve on a mainnet fork against the live PoolManager, hook and SHIFT token. The swap executed and every SHIFT went to the dead address, so the one external dependency the specialists could not exercise now has evidence.\n\n**Findings kept, after reproduction and merging:**\n- **Low, merged from permissions and economics:** funds parked at a deposit address before registration follow whichever owner the operator registers first, so a compromised operator can take them without any cap. The attached proof fails on this tree as stated.\n- **Low, new:** the vendored OpenZeppelin 5.1.0 signature checker sends any signer with code to ERC-1271 only. An EIP-7702 delegated wallet without that interface cannot register, be made owner, or relay purchases. I hit this live on the fork and reproduced it offline.\n- **Low, merged from math and permissions:** setting the purchase nonce to the maximum value bricks relayed purchases for that account permanently.\n- **Low:** owner and operator relayed purchases share one nonce, so either side cancels the other's outstanding signature.\n- **Low:** calling setCaps to lower one cap restarts the 24 h delay of the other cap's pending raise.\n- **Info:** zero-amount sweep events, fee rounding on tiny pots, even-amount requirement, the push dependency on the fee splitter, and the operator's unconstrained prize root as a trust assumption.\n\n**Dropped or downgraded.** Nothing from the specialists failed to reproduce. All static-analysis leads were false positives: the ETH recipients are the owner or fixed beneficiaries, the reentrancy lines sit under the outer guard, the hash-collision line uses fixed-size inputs, and the \"uninitialized\" state is a mapping.\n\n**Manifest.** The launch file has only the schema keys, one contract with no constructor parameters, and the owner and all roles are compile-time constants rather than the factory. Runtime and creation sizes are within limits.","treeHash":null,"usage":{"cachedInputTokens":1477415,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":52083,"runtime":"claude","turns":32,"wallClockMs":996927}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"aa333626543c4dee","findings":[{"citation":"resolved","description":"Area: Economic Security / Invariant / Flow Gap. The brief's only defence against a compromised operator is the rolling 24 h stake cap (0.05 ETH, 100 IMD, 2,500,000 SHIFT) plus the per-game cap, and the stated guarantee is that player money can only go into games the player starts or back to the player's own wallet. That guarantee holds for every registered account (lockGame, buy and buyWithSig all pass through _consumeCap; settle, refundGame, claimPrize and withdraw only move funds between an account's own balances and its owner). It does not hold for value already parked at depositAddress(accountId) while accountOf[accountId] is still zero. depositAddress is a pure function of the id, so the web/Telegram client can display it as soon as the id is assigned, and exchanges deliver to it with no knowledge of the vault. register(accountId, owner, ...) binds an unused id to whichever owner the two signatures name; the operator key signs the operator half and can sign the owner half for its own wallet, so whoever holds the operator key can bind any unregistered id to itself first (front-running the player's own register transaction, which then reverts AlreadyRegistered), call sweep (anyone may) and withdraw the entire forwarder balance. No cap, freeze, delay or pause is consulted on that path, and the amount is bounded only by what was sent, not by DEFAULT_ETH_CAP. The symmetric honest-operator case is a liveness gap rather than theft: ETH or tokens sent to a never-registered id stay in the forwarder until the operator co-signs a registration for it, with no other recovery. The @dev note at line 318 ('Register the account before funding it') documents the precondition but nothing enforces it on chain. Preconditions: an operator key compromise (the brief's explicit threat model for caps) and a deposit that reaches the forwarder before the player's registration is mined; the player's own register call does not help because it is first-come-first-served on the id. Impact: loss of the full pre-registration deposit for that player, unbounded by the cap. Minimal fix preserving the design: make register bind the owner to the id in a way the operator alone cannot choose, e.g. derive the required accountId from the owner's address (accountId = keccak256(owner, salt) checked in register) so a forwarder address is only ever creditable to the wallet it was generated for; or have the client only reveal depositAddress after Registered is observed and state that in the brief. Coverage note for the judge: within the assigned area I traced the conservation invariant totalPlayerFunds + pot + reservedPrizes + burnReserve <= holdings through every state-changing path (deposit, sweep, withdraw, lock, settle, refund, buy, buyDirect, burn, post/claim/expire) and found no leak; the per-asset split is exact for even amounts; the cap check (amount > perGame || amount > daily || spent > daily - amount) and the checkpoint binary search are consistent with the view; raise/unfreeze delays cannot be shortened by replacing or cancelling; the fee is gamePot/20 with dust to the winner; the v4 callback's delta decoding and exact-input check match v4 BalanceDelta packing and the hook's beforeSwapReturnDelta semantics. Mainnet dependencies were verified over RPC: the ETH/SHIFT pool under the exact key is initialised, IMD and SHIFT are plain 18-decimal ERC-20s (IMD is a LayerZero OFT, no tax), and the fee splitter accepts a raw ETH call (21,220 gas). Not reached in budget: a full fuzz run of VaultInvariant.t.sol and a live-fork execution of burnReserve against the real hook.","line":285,"path":"src/NightshiftVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {NightshiftVault} from \"src/NightshiftVault.sol\";\n\n/// @dev Funds parked at depositAddress(id) before register(id, ...) lands are outside the cap model:\n///      the operator alone can register that id to its own wallet, sweep and withdraw everything.\ncontract PreRegistrationForwarderTest is Test {\n    NightshiftVault vault;\n    uint256 constant OP_KEY = 0x0a011;\n    uint256 constant USER_KEY = 0xa11ce;\n    address op;\n    address user;\n    bytes32 constant ID = keccak256(\"new player\");\n\n    function setUp() public {\n        vm.warp(10 days);\n        op = vm.addr(OP_KEY);\n        user = vm.addr(USER_KEY);\n        vault = new NightshiftVault();\n        vm.prank(vault.OWNER());\n        vault.scheduleRoleChange(NightshiftVault.Role.Operator, op);\n        vm.warp(vm.getBlockTimestamp() + 48 hours);\n    }\n\n    function _sig(uint256 key, bytes32 digest) internal pure returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, digest);\n        return abi.encodePacked(r, s, v);\n    }\n\n    function _digest(bytes32 id, address owner, uint256 deadline) internal view returns (bytes32) {\n        return keccak256(\n            abi.encodePacked(\n                \"\\x19\\x01\",\n                vault.DOMAIN_SEPARATOR(),\n                keccak256(abi.encode(vault.REGISTER_TYPEHASH(), id, owner, deadline))\n            )\n        );\n    }\n\n    function test_operatorTakesPreRegistrationDepositUnbounded() public {\n        // Player funds the counterfactual address with 1 ETH (20x the 0.05 ETH daily cap).\n        address forwarder = vault.depositAddress(ID);\n        vm.deal(forwarder, 1 ether);\n\n        // The operator registers the same id to its own wallet: it signs both messages itself.\n        uint256 deadline = vm.getBlockTimestamp() + 1 hours;\n        bytes32 digest = _digest(ID, op, deadline);\n        vault.register(ID, op, deadline, _sig(OP_KEY, digest), _sig(OP_KEY, digest));\n\n        // The player's own registration can no longer land.\n        bytes32 userDigest = _digest(ID, user, deadline);\n        vm.expectRevert(NightshiftVault.AlreadyRegistered.selector);\n        vault.register(ID, user, deadline, _sig(USER_KEY, userDigest), _sig(OP_KEY, userDigest));\n\n        vault.sweep(ID, address(0));\n        vm.prank(op);\n        vault.withdraw(ID, address(0), 1 ether);\n\n        // Expected under the cap model: at most DEFAULT_ETH_CAP (0.05 ETH) per 24 h leaves the player.\n        // Actual: the whole 1 ETH is in the operator's wallet in one transaction sequence.\n        assertLe(op.balance, vault.DEFAULT_ETH_CAP(), \"operator extracted more than the daily cap\");\n    }\n}","reproduction":"State: fresh vault, operator role = op (after the 48 h delay), ID = keccak256('new player') unregistered. 1) vm.deal(vault.depositAddress(ID), 1 ether) (the player's exchange withdrawal lands first). 2) op calls register(ID, op, deadline, sig(op key over Register(ID, op, deadline)), same sig) -> succeeds. 3) the player's register(ID, user, deadline, userSig, opSig) -> reverts AlreadyRegistered. 4) anyone calls sweep(ID, address(0)) -> credits 1 ETH to ID. 5) op calls withdraw(ID, address(0), 1 ether) -> 1 ETH to op. Expected under the brief: a compromised operator moves at most DEFAULT_ETH_CAP = 0.05 ETH of a player's money per 24 h. Actual: op.balance == 1 ether in one sequence; assertLe(op.balance, 0.05 ether) fails with '1000000000000000000 > 50000000000000000'. Run: forge test --match-path test/scratch/PreRegistrationForwarder.t.sol","severity":"low","snippet":"        _signature(operator(), digest, operatorSig);","title":"Funds sent to a deposit address before its account is registered are outside the compromised-operator cap model"}],"hash":"8bea8a976b9ddff789a541dfdaed8a421d70fe05b346a3056e4605d365a10831","nodeId":"13724609-2ac0-464e-a66b-5a70dbc8c525","outcome":"completed","summary":"The review is complete and the findings file is written and validated.\n\n**Outcome.** The economics of NightshiftVault hold up well. I traced the conservation invariant (player funds + pot + reserved prizes + burn reserve never exceed holdings) through every state-changing path and found no leak. Purchase splits are exact, the 5% fee rounds dust to the winner, the rolling cap check matches its view function, cap raises and unfreezes cannot be shortened, and the Uniswap v4 callback's delta decoding and exact-input check are correct for the hook's permissions. The existing 54 unit tests pass. I also verified the hardcoded mainnet dependencies over RPC: the ETH/SHIFT pool under that exact key is initialised, IMD and SHIFT are plain 18-decimal ERC-20s with no tax, and the fee splitter accepts a raw ETH call.\n\n**One finding, rated low**, with a failing Foundry proof under `test/scratch/PreRegistrationForwarder.t.sol`:\n\n- **Pre-registration deposits escape the compromised-operator cap model.** A deposit address is computable before registration, and `register` binds any unused id to whichever owner the operator key plus any owner key names. A compromised operator can register a funded but unregistered id to its own wallet, front-running the player's own registration, then sweep and withdraw the whole balance. The test shows 1 ETH extracted against a 0.05 ETH daily cap. The honest-operator variant is a liveness gap: funds sent to a never-registered id stay in the forwarder with no other recovery. The suggested fix derives the required account id from the owner's address so a forwarder can only ever be credited to the wallet it was generated for.\n\n**Coverage.** All 27 listed entry points have a row: 26 hold, `register` carries the finding. Two invariant rows were added. Not reached within budget: a full fuzz run of the invariant suite and a live-fork execution of `burnReserve` against the real hook. Static analysis leads (reentrancy in sweep, arbitrary ETH send, uninitialized `_spending`, encodePacked collision) were each checked and dismissed as false positives given the reentrancy guard, fixed recipients, mapping semantics and fixed-length encoding.","treeHash":null,"usage":{"cachedInputTokens":919486,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":45311,"runtime":"claude","turns":23,"wallClockMs":1167814}},{"artifacts":[],"attempt":1,"bundleHash":"2a3b1870bb5473e06409ac3dd75ed46ff21b00e10e8ac56b0384e5f1b74f99cf","device":"90235813fd33f6ff","findings":[],"hash":"a18e2b0f30cb852407b923f988ddc47e4fec04bcb24e447a5eac0af1e4981791","nodeId":"859c3242-fdc2-4699-ba94-03ccb8a0c2aa","outcome":"completed","summary":"Implemented NightshiftVault with CREATE2 forwarders, vendored dependencies, tests, and [deployment documentation](README.md).\n\nValidation passed: `forge build`, all **56 tests**, and `forge fmt --check`, including stateful invariants and local Uniswap v4 integration.\n\nPrepared for IdentityMD’s review and deployment step. No live transaction was broadcast.","treeHash":"92787bb481879b693bd733bd11e9acbd04403de7","usage":{"cachedInputTokens":5636736,"inputTokens":156494,"model":"gpt-6-astra","outputTokens":70554,"runtime":"codex","turns":10,"wallClockMs":1716753}},{"artifacts":[],"attempt":1,"bundleHash":"84e12fcff74a098aae14e5f0df26d4f28fe68f14057c187188798407fb468e6a","device":"4dd9389cdce7acfe","findings":[],"hash":"d0142f2bb8285933c0bf9e0f6c0b7c7483b3f7d093f3da1c5b68cd612f807125","nodeId":"b1d34056-dbfc-43dc-bb3b-2a3aebb52c2f","outcome":"completed","summary":"Added 18 regression tests and strengthened stateful invariants with independent account ledgers, ownership changes, donations, and unauthorized withdrawals.\n\n`forge build` and all 77 tests pass with the required seed. Formatting and diff checks pass. Only `test/` changed.\n\nLive pool and hook verification remains outside the offline suite.","treeHash":"c72bfb16e0463f76000f4d0b7a0726f5fdd1c209","usage":{"cachedInputTokens":1868672,"inputTokens":124493,"model":"gpt-6.1-sol","outputTokens":15011,"runtime":"codex","turns":7,"wallClockMs":355404}},{"artifacts":[],"attempt":1,"bundleHash":"3269707699ac35fb120f1f93698bb59f89435705769965231c998fb2038d7706","device":"8d26cb1a4eaeb75a","findings":[],"hash":"eb29997f64513f333035176d5754ca4de058cc67a5315cd425152864237e3856","nodeId":"a4176e58-be4c-4124-afef-0710567e3b59","outcome":"completed","summary":"Done. `launch.json` is written and validated, and the working tree contains no other changes.\n\n**What the manifest declares**\n\n- One application contract, `NightshiftVault`, with an empty constructor argument list.\n- The accepted source at `src/NightshiftVault.sol:188` has a no-argument constructor. The contract owner, initial role holder, asset addresses, fee splitter, BurnExecutor, dead address, pool key and default caps are all compile-time constants, so no `$owner` argument exists to fill. The hard-coded owner matches the launch owner stated in the brief.\n- `DepositForwarder` is not listed. The vault deploys it on demand through CREATE2, and the README's deployment handoff says the same.\n- Notes are explanatory only, well under the 4000 character limit.\n\n**Verification**\n\n- `forge build` compiles cleanly.\n- `forge test` with the verifier's fuzz seed passed all 56 tests across 7 suites, including the deployment rehearsal that checks the fixed owner, predictable CREATE2 address and forbidden-opcode scan.\n- Manifest checked for exact keys, kind, string arguments, identifier length and notes length.\n- `git status --porcelain` shows only the new `launch.json`.","treeHash":"28cd30d9b529871199132190d04b0f6e447772ce","usage":{"cachedInputTokens":291869,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":4073,"runtime":"claude","turns":13,"wallClockMs":100108}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"76f57db4177175be","findings":[{"citation":"resolved","description":"buyWithSig binds every Purchase signature to purchaseNonces[accountId] (line 541) and increments it on use (line 548) regardless of whether the signer was the account owner or the operator. The two signers therefore race for the same sequence number. An operator-signed purchase relayed first consumes the nonce an owner's outstanding signature was built on, and the owner's signature can never be used (it would need re-signing with the new nonce). The operator is the party the caps are meant to bound; here it can also cancel any owner-authorised relayed purchase at will, and conversely an owner purchase cancels any outstanding operator signature. Impact is liveness/griefing only; no funds move wrongly. Fix: keep separate nonces per signer (e.g. mapping(bytes32 => mapping(address => uint256))) or include the signer in the nonce key.","line":548,"path":"src/NightshiftVault.sol","reproduction":"State: account A registered to alice, A holds 10 IMD. 1) alice signs Purchase(A, ITEM, IMD, 4e18, signer=alice, nonce=0, ownerNonce=0, deadline). 2) operator signs Purchase(A, ITEM, IMD, 2e18, signer=op, nonce=0, ownerNonce=0, deadline) and anyone calls buyWithSig(A, ITEM, IMD, 2e18, op, deadline, opSig): succeeds, purchaseNonces[A]=1. 3) buyWithSig(A, ITEM, IMD, 4e18, alice, deadline, aliceSig) reverts InvalidSignature. Expected: an owner-authorised purchase should not be cancellable by the operator. Verified with a scratch Foundry test (test_operatorConsumesOwnerPurchaseNonce) on this tree.","severity":"low","snippet":"        ++purchaseNonces[accountId];","title":"Owner and operator purchase signatures share one nonce, so the operator can invalidate an owner-signed purchase"},{"citation":"resolved","description":"Every setCaps call writes a fresh CapConfig and recomputes readyAt from block.timestamp. If an owner has a pending daily raise and then calls setCaps again to lower only perGame (passing the same, still-pending, higher daily value), the daily raise is treated as a new increase and its 24 h clock restarts. The owner cannot lower one limit without postponing the other limit's already-scheduled raise. Lowering is supposed to be instant and raising to take 24 h; here lowering costs the owner extra waiting on an unrelated raise. Impact: liveness/UX only, caps stay conservative. Fix: keep the existing readyAt when the new pending values are <= the already pending values, or track readyAt per cap.","line":403,"path":"src/NightshiftVault.sol","reproduction":"State: account A registered, default ETH caps (0.05 daily, 0.01 perGame). t0: alice calls setCaps(A, ETH, 1 ether, 0.01 ether) -> pending daily 1 ETH, readyAt t0+24h. t0+23h: alice calls setCaps(A, ETH, 1 ether, 0.005 ether) to tighten perGame. t0+24h+1s: caps(A, ETH) returns daily 0.05 ETH (raise restarted to t0+47h). Expected: daily 1 ETH at t0+24h since that raise was scheduled 24 h earlier. Verified with scratch test test_setCapsLowerOneResetsPendingRaiseOfOther on this tree.","severity":"low","snippet":"        uint256 readyAt = daily > immediateDaily || perGame > immediateGame ? block.timestamp + CAP_DELAY : 0;\n        _caps[accountId][asset] = CapConfig(true, immediateDaily, immediateGame, daily, perGame, readyAt);","title":"setCaps replaces the whole pending change, so lowering one cap restarts the 24 h delay of the other cap's pending raise"},{"citation":"resolved","description":"sweep does not short-circuit when the forwarder holds nothing. It still calls _creditDeposit with amount 0, which emits Deposited(accountId, asset, forwarder, 0) (and the forwarder emits Forwarded with 0). Indexers counting deposit events or deposit counts per account will see phantom zero deposits, and anyone can spam them for any registered account. No accounting changes. Fix: return early (or revert InvalidAmount) when amount == 0.","line":341,"path":"src/NightshiftVault.sol","reproduction":"State: account A registered, nothing sent to depositAddress(A). Anyone calls sweep(A, address(0)). Actual: returns 0 and emits Deposited(A, 0x0, forwarder, 0). Expected: no Deposited event for a zero credit. Verified with scratch test test_emptySweepEmitsZeroDeposit on this tree.","severity":"info","snippet":"        _creditDeposit(accountId, asset, amount, forwarder);","title":"Sweeping an empty deposit address emits Deposited with amount 0"},{"citation":"resolved","description":"settle sends the fee in the same transaction as crediting the winner. For ETH games, a revert in FEE_SPLITTER's receive path reverts settlement; the only remaining exit is refundGame after expiresAt + 24 h, which returns both stakes and discards the signed result, so the winner loses their winnings. I checked the live fee splitter 0x1119d5Df2f046bC2d3c0F6279EF2E9cE49422d44 on a mainnet fork: a plain 0.01 ETH call succeeds today and it accepts IMD and SHIFT transfers, so this is a trust assumption on an owned external contract (it exposes transferOwnership), not a present-day failure. The spec asks for push to the fee splitter, so this is recorded for the author's awareness; a pull-based fee accrual (accumulate fees, let the splitter collect) would remove the dependency.","line":502,"path":"src/NightshiftVault.sol","reproduction":"State: ETH game locked between A and B. Etch a contract whose receive() reverts at FEE_SPLITTER (vm.etch), then call settle(gameId, 0, deadline, validResultSig). Actual: revert ETHTransferFailed; the winner's balance is unchanged and after expiresAt + 24 h refundGame returns both stakes. Expected per design: winner credited 95%. The existing test test_feeFailureRollsBackSettlementAndRefundRemainsAvailable already demonstrates the rollback.","severity":"info","snippet":"        if (fee != 0) _send(game.asset, FEE_SPLITTER, fee);","title":"Settlement pushes the 5% fee to FEE_SPLITTER; if that external contract ever rejects ETH, winners cannot be paid and only a refund remains"}],"hash":"f7fe4208447c4942dd5bd780e29b21427b852693c9dadf232ea8f89296c2c104","nodeId":"8696fade-8c7d-4d8a-87ee-8da425000df2","outcome":"completed","summary":"The review is complete. The findings file is written, validated against the tree, and the scratch tests are removed so no tracked file changed.\n\n**Outcome: no critical, high or medium defects found in the control-flow area.** The file `.imd-findings.json` holds two low and two info findings plus a coverage row for all 27 entry points and two invariants.\n\n**What I checked**\n\n- Traced every state-changing entry point of `NightshiftVault` and `DepositForwarder` through its guards, state writes, external calls and failure paths, against the Execution Trace, Periphery and First Principles guides.\n- Ran the existing suite: 56 tests pass, including the invariant suite with revert-on-failure enabled.\n- Ran a mainnet fork probe: the hardcoded Uniswap v4 pool is initialised, the hook at the fixed address permits the vault's exact-input swap, the fee splitter accepts plain ETH plus IMD and SHIFT, and the IMD burn executor accepts IMD. The slither and aderyn leads (arbitrary ETH send, reentrancy in sweep and the callback, packed-encoding collision) all resolve to intended behaviour with guards in place.\n\n**Findings reported**\n\n1. **Low.** Owner and operator purchase signatures share one nonce in `buyWithSig`, so an operator-signed purchase relayed first invalidates an outstanding owner-signed one. Reproduced with a scratch test.\n2. **Low.** `setCaps` replaces the whole pending change, so lowering only the per-game cap restarts the 24 h clock on a pending daily raise. Reproduced with a scratch test.\n3. **Info.** Sweeping an empty deposit address emits a zero-amount `Deposited` event anyone can spam.\n4. **Info.** The 5% fee is pushed to the external fee splitter inside `settle`. It accepts ETH today, but if it ever rejected ETH, winners could only be refunded. This is a trust assumption on an owned external contract, recorded for awareness.\n\n**Not reached.** I did not reverse the hook's bytecode to confirm it has no sender allowlist that could later block the vault's burn swap. The fork run today succeeds, so this is only a forward-looking dependency note.","treeHash":null,"usage":{"cachedInputTokens":1250417,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":44568,"runtime":"claude","turns":26,"wallClockMs":626548}}],"verification":[{"checks":[{"durationMs":35599,"exitCode":0,"name":"build","output":"Compiling 104 files with Solc 0.8.26\nSolc 0.8.26 finished in 35.48s\nCompiler run successful!\n","passed":true},{"durationMs":3877,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/RealV4Integration.t.sol:RealV4IntegrationTest\n[PASS] test_realV4MinimumOutputRollbackPreservesReserveAndPool() (gas: 576278)\n[PASS] test_realV4SwapSettlesDeltasCallsHookAndBurnsOnlyNewOutput() (gas: 952359)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.00ms (843.48µs CPU time)\n\nRan 11 tests for test/PrizesAndRoles.t.sol:PrizesAndRolesTest\n[PASS] testFuzz_prizeConservation(uint96,uint96,uint8) (runs: 256, μ: 821347, ~: 882650)\nLogs:\n  Bound result 229086087\n  Bound result 1886\n\n[PASS] test_badProofsAndRootTotalsAreBoundedByReservation() (gas: 1566002)\n[PASS] test_claimsWhilePausedFrozenAndOwnerChanged() (gas: 1088722)\n[PASS] test_initialRolesAndOwnerAreFixedIndependentOfDeployer() (gas: 68631)\n[PASS] test_lastSecondClaimAndFullyClaimedExpiry() (gas: 835428)\n[PASS] test_pauseScopeAndOwnerCannotMoveOrRedirectPlayerFunds() (gas: 1145266)\n[PASS] test_postsClaimsAndExpiresOnlyUnclaimedAllAssets() (gas: 3466157)\n[PASS] test_prizePostingCannotReservePlayerMoneyBurnReserveOrOtherAsset() (gas: 1042689)\n[PASS] test_resultSignerRotationRejectsFormerSignerForExistingGames() (gas: 1040864)\n[PASS] test_roleChangesOnlyOwnerAndTakeFull48Hours() (gas: 605319)\n[PASS] test_roleReplacementRestartsDelayCancellationAndNonzeroValidation() (gas: 319338)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 41.16ms (48.84ms CPU time)\n\nRan 2 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_factoryDeploymentEmptyChainFixedOwnerAndPredictableAddress() (gas: 14596198)\n[PASS] test_forwarderCreate2PredictionIncludesVaultAccountAndCreationCode() (gas: 11418)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 58.29ms (58.45ms CPU time)\n\nRan 13 tests for test/ShopAndBurn.t.sol:ShopAndBurnTest\n[PASS] testFuzz_exactPurchaseHalves(uint96,uint8,bool) (runs: 256, μ: 504784, ~: 522924)\nLogs:\n  Bound result 1036800\n\n[PASS] test_burnMustCallbackOnceAndTransferOutput() (gas: 1033735)\n[PASS] test_burnOnlyKeeperReserveLimitsAndCallbackAuthentication() (gas: 634993)\n[PASS] test_burnSlippagePartialFillAndEmptyOutputRevertAtomically() (gas: 981912)\n[PASS] test_burnStillWorksPaused() (gas: 700285)\n[PASS] test_failedPurchaseRecipientTransferRollsBackEverything() (gas: 726619)\n[PASS] test_operatorShopAndGamesShareDailyCapAllAssets() (gas: 3135169)\n[PASS] test_ownerAndDirectPurchasesSplitExactlyAllAssets() (gas: 2163598)\n[PASS] test_ownerChangeInvalidatesOperatorPurchaseSignature() (gas: 654381)\n[PASS] test_purchaseInputAndAuthorityFailuresAndFrozenDirectPayer() (gas: 492898)\n[PASS] test_purchaseSignatureCannotChangeItemAmountOrAssetAndCanBeCancelled() (gas: 579176)\n[PASS] test_relayOwnerAndOperatorSignaturesAndReplayProtection() (gas: 1066248)\n[PASS] test_reserveBurnUsesExactPoolPaysETHAndBurnsAllSHIFT() (gas: 1012921)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 105.01ms (39.17ms CPU time)\n\nRan 12 tests for test/Accounts.t.sol:AccountsTest\n[PASS] test_counterfactualTokenDepositAndCrossAccountIsolation() (gas: 1074103)\n[PASS] test_depositSweepAndWithdrawAllAssets() (gas: 2476560)\n[PASS] test_falseReturningAndFeeTokensCannotInflateBalances() (gas: 804866)\n[PASS] test_onlyOwnerCanChangeOwnerWithNewOwnersSignatureAndNonce() (gas: 660996)\n[PASS] test_ownerChangeRedirectsOnlyToConsentingNewOwner() (gas: 682966)\n[PASS] test_reentrantAndRejectingOwnerCannotDrainOrLoseWithdrawal() (gas: 912838)\n[PASS] test_registrationAcceptsERC1271OwnerAndOperator() (gas: 1033057)\n[PASS] test_registrationExpiryDomainAndInputValidation() (gas: 217678)\n[PASS] test_registrationRequiresBothSignaturesAndCannotReplay() (gas: 288612)\n[PASS] test_tokenCallbackCannotReenterDeposit() (gas: 655998)\n[PASS] test_unsupportedAssetZeroAmountsUnknownAccountsAndUnlabelledETH() (gas: 218910)\n[PASS] test_withdrawOnlyOwnerWhilePausedAndFrozenAllAssets() (gas: 1378718)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 124.25ms (13.07ms CPU time)\n\nRan 14 tests for test/GamesAndCaps.t.sol:GamesAndCapsTest\n[PASS] testFuzz_feeConservesEveryWei(uint96,uint8,bool) (runs: 256, μ: 1121347, ~: 1243715)\nLogs:\n  Bound result 4587169912387108379\n\n[PASS] testFuzz_rollingWindowMatchesIndependentHistory(uint32[12],uint64[12]) (runs: 256, μ: 5805608, ~: 5835277)\n[PASS] test_capChangesLowerImmediatelyRaiseAfterDelayReplaceAndMixedChange() (gas: 496914)\n[PASS] test_dailyAndPerGameCapsAllAssets() (gas: 7202724)\n[PASS] test_exactRollingWindowDoesNotResetAtMidnightOrFirstSpendAnniversary() (gas: 2217794)\n[PASS] test_feeFailureRollsBackSettlementAndRefundRemainsAvailable() (gas: 1144415)\n[PASS] test_freezeImmediateUnfreezeDelayAndRefreezeCancel() (gas: 1163071)\n[PASS] test_gameResultsAndFivePercentFeeAllAssets() (gas: 9858389)\n[PASS] test_lockRejectsUnauthorizedDuplicateSameOwnerInvalidStakeAndExpiry() (gas: 1084890)\n[PASS] test_loweringCapsBelowExistingSpendDoesNotUnderflowOrEraseHistory() (gas: 1498498)\n[PASS] test_ownerChangePreservesSpendAndCancelsPendingRelaxation() (gas: 1605624)\n[PASS] test_refundBoundaryAndSettlementCutoffAllAssets() (gas: 3130336)\n[PASS] test_settleWorksPausedAndFrozen() (gas: 1323537)\n[PASS] test_wrongSignerExpiredSignatureWrongDomainInvalidResultAndDoubleSettlement() (gas: 1207701)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 124.34ms (197.65ms CPU time)\n\nRan 2 tests for test/VaultInvariant.t.sol:VaultInvariantTest\n[PASS]\nVaultInvariantTest invariants:\n[PASS] invariant_allLiabilitiesBackedAndExternalFlowsConserved\n[PASS] invariant_epochReservationsEqualUnclaimedPrizes\n[PASS] invariant_lockedBalancesEqualAllLiveGameStakes\n VaultInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭--------------+----------------+-------+---------+----------╮\n| Contract     | Selector       | Calls | Reverts | Discards |\n+============================================================+\n| VaultHandler | advanceTime    | 579   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | burn           | 593   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | buy            | 591   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | buyDirect      | 620   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | claimOrExpire  | 566   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | deposit        | 539   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | freeze         | 616   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | lock           | 531   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | pause          | 600   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | post           | 603   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | setCaps        | 634   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | settleOrRefund | 584   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | sweep          | 544   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | withdraw       | 592   | 0       | 0        |\n╰--------------+----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 86400\n  Bound result 9089\n  Bound result 12184\n  Bound result 14604\n  Bound result 55684\n  Bound result 2066\n  Bound result 13\n\n[PASS] test_handlerTraversesAllEconomicTransitions() (gas: 4273562)\nLogs:\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 10000000000000000\n  Bound result 1000000000000000000\n  Bound result 100000000000000000\n  Bound result 100000000000000000\n  Bound result 10000000000000000000\n  Bound result 1000000000000000000\n  Bound result 500000000000000000\n  Bound result 1000000000000000000\n  Bound result 2678400\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.74s (3.73s CPU time)\n\nRan 7 test suites in 3.74s (4.20s CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":71,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"DepositForwarder.receive()\",\"DepositForwarder.sweep(address)\",\"NightshiftVault.applyRoleChange(uint8)\",\"NightshiftVault.burnReserve(uint256,uint256)\",\"NightshiftVault.buy(bytes32,bytes32,address,uint256)\",\"NightshiftVault.buyDirect(bytes32,bytes32,address,uint256)\",\"NightshiftVault.buyWithSig(bytes32,bytes32,address,uint256,address,uint256,bytes)\",\"NightshiftVault.cancelRoleChange(uint8)\",\"NightshiftVault.claimPrize(uint256,bytes32,address,uint256,bytes32[])\",\"NightshiftVault.deposit(bytes32,address,uint256)\",\"NightshiftVault.depositETH(bytes32)\",\"NightshiftVault.expirePrizes(uint256,address)\",\"NightshiftVault.invalidatePurchaseNonce(bytes32,uint256)\",\"NightshiftVault.lockGame(bytes32,bytes32,bytes32,address,uint256,uint256)\",\"NightshiftVault.postPrizes(uint256,bytes32,address,uint256)\",\"NightshiftVault.receive()\",\"NightshiftVault.refundGame(bytes32)\",\"NightshiftVault.register(bytes32,address,uint256,bytes,bytes)\",\"NightshiftVault.scheduleRoleChange(uint8,address)\",\"NightshiftVault.setCaps(bytes32,address,uint256,uint256)\",\"NightshiftVault.setFrozen(bytes32,bool)\",\"NightshiftVault.setOwner(bytes32,address,uint256,bytes)\",\"NightshiftVault.setPaused(bool)\",\"NightshiftVault.settle(bytes32,uint8,uint256,bytes)\",\"NightshiftVault.sweep(bytes32,address)\",\"NightshiftVault.unlockCallback(bytes)\",\"NightshiftVault.withdraw(bytes32,address,uint256)\"],\"files\":{\".gitignore\":7,\"LICENSE\":23,\"README.md\":152,\"docs/DEPENDENCIES.md\":20,\"docs/INTEGRATION.md\":118,\"docs/SECURITY.md\":77,\"docs/VALIDATION.md\":29,\"foundry.toml\":23,\"remappings.txt\":4,\"src/DepositForwarder.sol\":48,\"src/NightshiftVault.sol\":804,\"src/interfaces/IV4PoolManager.sol\":28,\"test/Accounts.t.sol\":228,\"test/Deployment.t.sol\":67,\"test/GamesAndCaps.t.sol\":345,\"test/PrizesAndRoles.t.sol\":273,\"test/RealV4Integration.t.sol\":138,\"test/ShopAndBurn.t.sol\":245,\"test/VaultInvariant.t.sol\":357,\"test/VaultTestBase.sol\":178,\"test/mocks/MockAssets.sol\":107,\"test/mocks/MockV4PoolManager.sol\":88},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4527,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/NightshiftVault.sol:748: NightshiftVault._send(address,address,uint256) (src/NightshiftVault.sol#748-755) sends eth to arbitrary user\n[high/medium] reentrancy-balance at src/NightshiftVault.sol:604: Reentrancy in NightshiftVault.unlockCallback(bytes) (src/NightshiftVault.sol#604-629):\n[high/medium] reentrancy-balance at src/NightshiftVault.sol:327: Reentrancy in NightshiftVault.sweep(bytes32,address) (src/NightshiftVault.sol#327-342):\n[high/high] uninitialized-state at src/NightshiftVault.sol:431: NightshiftVault._spending (src/NightshiftVault.sol#116) is never initialized. It is used in:\n[medium/high] incorrect-equality at src/NightshiftVault.sol:757: NightshiftVault._consumeCap(bytes32,address,uint256,bool) (src/NightshiftVault.sol#757-769) uses a dangerous strict equality:\n[medium/high] write-after-write at src/NightshiftVault.sol:127: NightshiftVault._receivingForwarder (src/NightshiftVault.sol#127) is written in both\n[low/medium] reentrancy-benign at src/NightshiftVault.sol:327: Reentrancy in NightshiftVault.sweep(bytes32,address) (src/NightshiftVault.sol#327-342):\n[low/medium] timestamp at src/NightshiftVault.sol:706: NightshiftVault._deadline(uint256) (src/NightshiftVault.sol#706-708) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:449: NightshiftVault.lockGame(bytes32,bytes32,bytes32,address,uint256,uint256) (src/NightshiftVault.sol#449-477) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:645: NightshiftVault.claimPrize(uint256,bytes32,address,uint256,bytes32[]) (src/NightshiftVault.sol#645-665) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:424: NightshiftVault.frozen(bytes32) (src/NightshiftVault.sol#424-427) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:668: NightshiftVault.expirePrizes(uint256,address) (src/NightshiftVault.sol#668-679) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:378: NightshiftVault.caps(bytes32,address) (src/NightshiftVault.sol#378-389) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:431: NightshiftVault.spentInLast24Hours(bytes32,address) (src/NightshiftVault.sol#431-446) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:481: NightshiftVault.settle(bytes32,uint8,uint256,bytes) (src/NightshiftVault.sol#481-503) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:243: NightshiftVault.cancelRoleChange(NightshiftVault.Role) (src/NightshiftVault.sol#243-249) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:757: NightshiftVault._consumeCap(bytes32,address,uint256,bool) (src/NightshiftVault.sol#757-769) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:687: NightshiftVault._role(NightshiftVault.Role) (src/NightshiftVault.sol#687-690) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:252: NightshiftVault.applyRoleChange(NightshiftVault.Role) (src/NightshiftVault.sol#252-260) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:506: NightshiftVault.refundGame(bytes32) (src/NightshiftVault.sol#506-514) uses timestamp for comparisons\n[low/medium] timestamp at src/NightshiftVault.sol:398: NightshiftVault.setCaps(bytes32,address,uint256,uint256) (src/NightshiftVault.sol#398-408) uses timestamp for comparisons","passed":true},{"durationMs":620,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/NightshiftVault.sol:322: `abi.encodePacked()` Hash Collision\n[high] reentrancy-state-change at src/NightshiftVault.sol:337: Reentrancy: State change after external call\n[low] centralization-risk at src/NightshiftVault.sol:236: Centralization Risk (3 places)\n[low] large-numeric-literal at src/NightshiftVault.sol:37: Large Numeric Literal\n[low] state-change-without-event at src/NightshiftVault.sol:604: State Change Without Event\n[low] unchecked-return at src/NightshiftVault.sol:328: Unchecked Return (8 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a18e2b0f30cb852407b923f988ddc47e4fec04bcb24e447a5eac0af1e4981791","verifiedTreeHash":"92787bb481879b693bd733bd11e9acbd04403de7","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":45411,"exitCode":0,"name":"build","output":"Compiling 105 files with Solc 0.8.26\nSolc 0.8.26 finished in 45.28s\nCompiler run successful!\n","passed":true},{"durationMs":7580,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/RealV4Integration.t.sol:RealV4IntegrationTest\n[PASS] test_realV4MinimumOutputRollbackPreservesReserveAndPool() (gas: 576278)\n[PASS] test_realV4SwapSettlesDeltasCallsHookAndBurnsOnlyNewOutput() (gas: 952359)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.32ms (905.22µs CPU time)\n\nRan 12 tests for test/Accounts.t.sol:AccountsTest\n[PASS] test_counterfactualTokenDepositAndCrossAccountIsolation() (gas: 1074103)\n[PASS] test_depositSweepAndWithdrawAllAssets() (gas: 2476560)\n[PASS] test_falseReturningAndFeeTokensCannotInflateBalances() (gas: 804866)\n[PASS] test_onlyOwnerCanChangeOwnerWithNewOwnersSignatureAndNonce() (gas: 660996)\n[PASS] test_ownerChangeRedirectsOnlyToConsentingNewOwner() (gas: 682966)\n[PASS] test_reentrantAndRejectingOwnerCannotDrainOrLoseWithdrawal() (gas: 912838)\n[PASS] test_registrationAcceptsERC1271OwnerAndOperator() (gas: 1033057)\n[PASS] test_registrationExpiryDomainAndInputValidation() (gas: 217678)\n[PASS] test_registrationRequiresBothSignaturesAndCannotReplay() (gas: 288612)\n[PASS] test_tokenCallbackCannotReenterDeposit() (gas: 655998)\n[PASS] test_unsupportedAssetZeroAmountsUnknownAccountsAndUnlabelledETH() (gas: 218910)\n[PASS] test_withdrawOnlyOwnerWhilePausedAndFrozenAllAssets() (gas: 1378718)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 10.43ms (8.99ms CPU time)\n\nRan 2 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_factoryDeploymentEmptyChainFixedOwnerAndPredictableAddress() (gas: 14596198)\n[PASS] test_forwarderCreate2PredictionIncludesVaultAccountAndCreationCode() (gas: 11418)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 44.37ms (44.51ms CPU time)\n\nRan 13 tests for test/ShopAndBurn.t.sol:ShopAndBurnTest\n[PASS] testFuzz_exactPurchaseHalves(uint96,uint8,bool) (runs: 256, μ: 505273, ~: 522924)\nLogs:\n  Bound result 11109\n\n[PASS] test_burnMustCallbackOnceAndTransferOutput() (gas: 1033735)\n[PASS] test_burnOnlyKeeperReserveLimitsAndCallbackAuthentication() (gas: 634993)\n[PASS] test_burnSlippagePartialFillAndEmptyOutputRevertAtomically() (gas: 981912)\n[PASS] test_burnStillWorksPaused() (gas: 700285)\n[PASS] test_failedPurchaseRecipientTransferRollsBackEverything() (gas: 726619)\n[PASS] test_operatorShopAndGamesShareDailyCapAllAssets() (gas: 3135169)\n[PASS] test_ownerAndDirectPurchasesSplitExactlyAllAssets() (gas: 2163598)\n[PASS] test_ownerChangeInvalidatesOperatorPurchaseSignature() (gas: 654381)\n[PASS] test_purchaseInputAndAuthorityFailuresAndFrozenDirectPayer() (gas: 492898)\n[PASS] test_purchaseSignatureCannotChangeItemAmountOrAssetAndCanBeCancelled() (gas: 579176)\n[PASS] test_relayOwnerAndOperatorSignaturesAndReplayProtection() (gas: 1066248)\n[PASS] test_reserveBurnUsesExactPoolPaysETHAndBurnsAllSHIFT() (gas: 1012921)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 50.17ms (57.14ms CPU time)\n\nRan 11 tests for test/PrizesAndRoles.t.sol:PrizesAndRolesTest\n[PASS] testFuzz_prizeConservation(uint96,uint96,uint8) (runs: 256, μ: 820157, ~: 882654)\nLogs:\n  Bound result 229086087\n  Bound result 1886\n\n[PASS] test_badProofsAndRootTotalsAreBoundedByReservation() (gas: 1566002)\n[PASS] test_claimsWhilePausedFrozenAndOwnerChanged() (gas: 1088722)\n[PASS] test_initialRolesAndOwnerAreFixedIndependentOfDeployer() (gas: 68631)\n[PASS] test_lastSecondClaimAndFullyClaimedExpiry() (gas: 835428)\n[PASS] test_pauseScopeAndOwnerCannotMoveOrRedirectPlayerFunds() (gas: 1145266)\n[PASS] test_postsClaimsAndExpiresOnlyUnclaimedAllAssets() (gas: 3466157)\n[PASS] test_prizePostingCannotReservePlayerMoneyBurnReserveOrOtherAsset() (gas: 1042689)\n[PASS] test_resultSignerRotationRejectsFormerSignerForExistingGames() (gas: 1040864)\n[PASS] test_roleChangesOnlyOwnerAndTakeFull48Hours() (gas: 605319)\n[PASS] test_roleReplacementRestartsDelayCancellationAndNonzeroValidation() (gas: 319338)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 120.15ms (53.32ms CPU time)\n\nRan 18 tests for test/AdversarialBoundaries.t.sol:AdversarialBoundariesTest\n[PASS] testFuzz_repeatedDepositWithdrawalRoundTripsCannotCreateValue(uint96,uint8) (runs: 1000, μ: 1584231, ~: 1876361)\nLogs:\n  Bound result 1\n\n[PASS] test_burnRejectsIncorrectSettlementAndShortOutputTransferAtomically() (gas: 1258378)\n[PASS] test_counterfactualFundsSurvivePauseAndOwnershipChangeWithoutRedirection() (gas: 1915880)\n[PASS] test_custodyPurchaseAndPrizeEventsIdentifyCorrectAccountAssetAndAmount() (gas: 2120468)\n[PASS] test_defaultCapsAreSpecifiedAndSeparateByAccountAndAsset() (gas: 1157637)\n[PASS] test_domainUsesRequiredNameVersionChainAndVault() (gas: 22406)\n[PASS] test_failedRelayedPurchaseKeepsNonceCapAndFundsThenSameSignatureWorks() (gas: 1879369)\n[PASS] test_failedTokenFeeTransferRestoresGameAndCanRetry() (gas: 2667211)\n[PASS] test_gameCanSettleAtLastGraceSecondButOnlyAvailableFundsWithdraw() (gas: 3144901)\n[PASS] test_keeperRotationChangesBurnPermissionAtExactBoundary() (gas: 978514)\n[PASS] test_legacyNoReturnTokenSupportsDepositSweepShopEscrowAndWithdrawal() (gas: 2914355)\n[PASS] test_lockFailureAtSecondPlayerRestoresFirstPlayerBalanceAndCapAllAssets() (gas: 5313217)\n[PASS] test_newOwnerContractMustConsentAndReceivesLockedGameWinnings() (gas: 2091560)\n[PASS] test_oldOwnerPurchaseCannotReplayEvenAfterOwnershipReturns() (gas: 1230729)\n[PASS] test_operatorRotationEnforcesActualAuthorityAtBoundary() (gas: 1763930)\n[PASS] test_registrationAtExactDeadlineAndMalformedSignatureRollback() (gas: 207547)\n[PASS] test_registrationRejectsMalleableOwnerSignature() (gas: 75400)\n[PASS] test_unfreezeReplacementRestartsFullDelayAndCapLoweringCancelsIncrease() (gas: 458267)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 120.51ms (142.57ms CPU time)\n\nRan 14 tests for test/GamesAndCaps.t.sol:GamesAndCapsTest\n[PASS] testFuzz_feeConservesEveryWei(uint96,uint8,bool) (runs: 256, μ: 1122336, ~: 1243715)\nLogs:\n  Bound result 4587169912387108379\n\n[PASS] testFuzz_rollingWindowMatchesIndependentHistory(uint32[12],uint64[12]) (runs: 256, μ: 5804820, ~: 5838177)\n[PASS] test_capChangesLowerImmediatelyRaiseAfterDelayReplaceAndMixedChange() (gas: 496914)\n[PASS] test_dailyAndPerGameCapsAllAssets() (gas: 7202724)\n[PASS] test_exactRollingWindowDoesNotResetAtMidnightOrFirstSpendAnniversary() (gas: 2217794)\n[PASS] test_feeFailureRollsBackSettlementAndRefundRemainsAvailable() (gas: 1144415)\n[PASS] test_freezeImmediateUnfreezeDelayAndRefreezeCancel() (gas: 1163071)\n[PASS] test_gameResultsAndFivePercentFeeAllAssets() (gas: 9858389)\n[PASS] test_lockRejectsUnauthorizedDuplicateSameOwnerInvalidStakeAndExpiry() (gas: 1084890)\n[PASS] test_loweringCapsBelowExistingSpendDoesNotUnderflowOrEraseHistory() (gas: 1498498)\n[PASS] test_ownerChangePreservesSpendAndCancelsPendingRelaxation() (gas: 1605624)\n[PASS] test_refundBoundaryAndSettlementCutoffAllAssets() (gas: 3130336)\n[PASS] test_settleWorksPausedAndFrozen() (gas: 1323537)\n[PASS] test_wrongSignerExpiredSignatureWrongDomainInvalidResultAndDoubleSettlement() (gas: 1207701)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 143.60ms (272.31ms CPU time)\n\nRan 5 tests for test/VaultInvariant.t.sol:VaultInvariantTest\n[PASS] invariant_allLiabilitiesBackedAndExternalFlowsConserved() (runs: 128, calls: 8192, reverts: 0)\n\n╭--------------+----------------------+-------+---------+----------╮\n| Contract     | Selector             | Calls | Reverts | Discards |\n+==================================================================+\n| VaultHandler | advanceTime          | 489   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | burn                 | 451   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | buy                  | 481   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | buyDirect            | 482   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | claimOrExpire        | 469   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | deposit              | 478   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | donate               | 505   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | freeze               | 439   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | lock                 | 465   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | pause                | 496   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | post                 | 498   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | rotateOwner          | 479   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | setCaps              | 450   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | settleOrRefund       | 521   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | sweep                | 490   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | unauthorizedWithdraw | 499   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | withdraw             | 500   | 0       | 0        |\n╰--------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 2437\n  Bound result 14805\n  Bound result 13615\n  Bound result 1\n  Bound result 15663\n  Bound result 15908\n  Bound result 14740\n  Bound result 694\n  Bound result 234122110708104\n  Bound result 18216\n  Bound result 5872\n  Bound result 1275\n  Bound result 1018\n  Bound result 7258\n  Bound result 2388102009186\n  Bound result 81008940\n  Bound result 51\n  Bound result 7434\n  Bound result 1044\n  Bound result 2175701\n  Bound result 4043469572\n  Bound result 742871629612076485\n  Bound result 2239\n  Bound result 4420\n  Bound result 518400\n  Bound result 1926\n  Bound result 2\n  Bound result 16848\n  Bound result 16304\n  Bound result 2413752425\n  Bound result 460037158597097998\n  Bound result 14630\n  Bound result 887\n  Bound result 47747\n\n[PASS] invariant_eachAccountMatchesIndependentActionLedger() (runs: 256, calls: 24576, reverts: 0)\n\n╭--------------+----------------------+-------+---------+----------╮\n| Contract     | Selector             | Calls | Reverts | Discards |\n+==================================================================+\n| VaultHandler | advanceTime          | 1463  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | burn                 | 1398  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | buy                  | 1441  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | buyDirect            | 1450  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | claimOrExpire        | 1479  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | deposit              | 1443  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | donate               | 1497  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | freeze               | 1379  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | lock                 | 1494  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | pause                | 1448  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | post                 | 1474  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | rotateOwner          | 1449  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | setCaps              | 1353  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | settleOrRefund       | 1480  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | sweep                | 1444  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | unauthorizedWithdraw | 1446  | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | withdraw             | 1438  | 0       | 0        |\n╰--------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 37\n  Bound result 1358361177149574210\n  Bound result 9306\n  Bound result 7258\n  Bound result 14475\n  Bound result 7563863462317342804\n  Bound result 18329\n  Bound result 2953\n  Bound result 2539\n  Bound result 19178\n  Bound result 16593\n  Bound result 81176250638981612\n  Bound result 11577\n  Bound result 534\n  Bound result 66\n  Bound result 8\n  Bound result 10938\n  Bound result 210607787\n  Bound result 2439\n  Bound result 9914\n  Bound result 389\n  Bound result 3\n  Bound result 344567114248003466\n  Bound result 13982\n  Bound result 13996\n  Bound result 7\n  Bound result 4268760065950\n  Bound result 3201\n  Bound result 16\n\n[PASS] invariant_epochReservationsEqualUnclaimedPrizes() (runs: 128, calls: 8192, reverts: 0)\n\n╭--------------+----------------------+-------+---------+----------╮\n| Contract     | Selector             | Calls | Reverts | Discards |\n+==================================================================+\n| VaultHandler | advanceTime          | 489   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | burn                 | 451   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | buy                  | 481   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | buyDirect            | 482   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | claimOrExpire        | 469   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | deposit              | 478   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | donate               | 505   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | freeze               | 439   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | lock                 | 465   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | pause                | 496   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | post                 | 498   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | rotateOwner          | 479   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | setCaps              | 450   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | settleOrRefund       | 521   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | sweep                | 490   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | unauthorizedWithdraw | 499   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | withdraw             | 500   | 0       | 0        |\n╰--------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 2437\n  Bound result 14805\n  Bound result 13615\n  Bound result 1\n  Bound result 15663\n  Bound result 15908\n  Bound result 14740\n  Bound result 694\n  Bound result 234122110708104\n  Bound result 18216\n  Bound result 5872\n  Bound result 1275\n  Bound result 1018\n  Bound result 7258\n  Bound result 2388102009186\n  Bound result 81008940\n  Bound result 51\n  Bound result 7434\n  Bound result 1042\n  Bound result 2175701\n  Bound result 4043469572\n  Bound result 742871629612076485\n  Bound result 2239\n  Bound result 4420\n  Bound result 518400\n  Bound result 1926\n  Bound result 2\n  Bound result 16848\n  Bound result 16304\n  Bound result 2413752423\n  Bound result 460037158597097998\n  Bound result 14630\n  Bound result 887\n  Bound result 47747\n\n[PASS] invariant_lockedBalancesEqualAllLiveGameStakes() (runs: 128, calls: 8192, reverts: 0)\n\n╭--------------+----------------------+-------+---------+----------╮\n| Contract     | Selector             | Calls | Reverts | Discards |\n+==================================================================+\n| VaultHandler | advanceTime          | 489   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | burn                 | 451   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | buy                  | 481   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | buyDirect            | 482   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | claimOrExpire        | 469   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | deposit              | 478   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | donate               | 505   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | freeze               | 439   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | lock                 | 465   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | pause                | 496   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | post                 | 498   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | rotateOwner          | 479   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | setCaps              | 450   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | settleOrRefund       | 521   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | sweep                | 490   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | unauthorizedWithdraw | 499   | 0       | 0        |\n|--------------+----------------------+-------+---------+----------|\n| VaultHandler | withdraw             | 500   | 0       | 0        |\n╰--------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 2437\n  Bound result 14805\n  Bound result 13615\n  Bound result 1\n  Bound result 15663\n  Bound result 15908\n  Bound result 14740\n  Bound result 694\n  Bound result 234122110708104\n  Bound result 18216\n  Bound result 5872\n  Bound result 1275\n  Bound result 1018\n  Bound result 7258\n  Bound result 2388102009186\n  Bound result 81008940\n  Bound result 51\n  Bound result 7434\n  Bound result 350\n  Bound result 2175701\n  Bound result 4078735570\n  Bound result 742871629612076485\n  Bound result 2239\n  Bound result 192\n  Bound result 518400\n  Bound result 2670\n  Bound result 2\n  Bound result 16950\n  Bound result 16304\n  Bound result 756\n  Bound result 460037158597097998\n  Bound result 14629\n  Bound result 887\n  Bound result 7882\n\n[PASS] test_handlerTraversesAllEconomicTransitions() (gas: 5903120)\nLogs:\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 10000000000000000\n  Bound result 1\n  Bound result 1000000000000000000\n  Bound result 100000000000000000\n  Bound result 100000000000000000\n  Bound result 10000000000000000000\n  Bound result 1000000000000000000\n  Bound result 500000000000000000\n  Bound result 1000000000000000000\n  Bound result 2678400\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 11\n  Bound result 13\n  Bound result 17\n  Bound result 1\n\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 7.42s (12.74s CPU time)\n\nRan 8 test suites in 7.42s (7.92s CPU time): 77 tests passed, 0 failed, 0 skipped (77 total tests)\n","passed":true},{"durationMs":91,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"DepositForwarder.receive()\",\"DepositForwarder.sweep(address)\",\"NightshiftVault.applyRoleChange(uint8)\",\"NightshiftVault.burnReserve(uint256,uint256)\",\"NightshiftVault.buy(bytes32,bytes32,address,uint256)\",\"NightshiftVault.buyDirect(bytes32,bytes32,address,uint256)\",\"NightshiftVault.buyWithSig(bytes32,bytes32,address,uint256,address,uint256,bytes)\",\"NightshiftVault.cancelRoleChange(uint8)\",\"NightshiftVault.claimPrize(uint256,bytes32,address,uint256,bytes32[])\",\"NightshiftVault.deposit(bytes32,address,uint256)\",\"NightshiftVault.depositETH(bytes32)\",\"NightshiftVault.expirePrizes(uint256,address)\",\"NightshiftVault.invalidatePurchaseNonce(bytes32,uint256)\",\"NightshiftVault.lockGame(bytes32,bytes32,bytes32,address,uint256,uint256)\",\"NightshiftVault.postPrizes(uint256,bytes32,address,uint256)\",\"NightshiftVault.receive()\",\"NightshiftVault.refundGame(bytes32)\",\"NightshiftVault.register(bytes32,address,uint256,bytes,bytes)\",\"NightshiftVault.scheduleRoleChange(uint8,address)\",\"NightshiftVault.setCaps(bytes32,address,uint256,uint256)\",\"NightshiftVault.setFrozen(bytes32,bool)\",\"NightshiftVault.setOwner(bytes32,address,uint256,bytes)\",\"NightshiftVault.setPaused(bool)\",\"NightshiftVault.settle(bytes32,uint8,uint256,bytes)\",\"NightshiftVault.sweep(bytes32,address)\",\"NightshiftVault.unlockCallback(bytes)\",\"NightshiftVault.withdraw(bytes32,address,uint256)\"],\"files\":{\".gitignore\":7,\"LICENSE\":23,\"README.md\":152,\"docs/DEPENDENCIES.md\":20,\"docs/INTEGRATION.md\":118,\"docs/SECURITY.md\":77,\"docs/VALIDATION.md\":29,\"foundry.toml\":23,\"remappings.txt\":4,\"src/DepositForwarder.sol\":48,\"src/NightshiftVault.sol\":804,\"src/interfaces/IV4PoolManager.sol\":28,\"test/Accounts.t.sol\":228,\"test/AdversarialBoundaries.t.sol\":453,\"test/Deployment.t.sol\":67,\"test/GamesAndCaps.t.sol\":345,\"test/PrizesAndRoles.t.sol\":273,\"test/RealV4Integration.t.sol\":138,\"test/ShopAndBurn.t.sol\":245,\"test/VaultInvariant.t.sol\":492,\"test/VaultTestBase.sol\":178,\"test/mocks/MockAssets.sol\":107,\"test/mocks/MockV4PoolManager.sol\":88},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d0142f2bb8285933c0bf9e0f6c0b7c7483b3f7d093f3da1c5b68cd612f807125","verifiedTreeHash":"c72bfb16e0463f76000f4d0b7a0726f5fdd1c209","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":36368,"exitCode":0,"name":"build","output":"Compiling 104 files with Solc 0.8.26\nSolc 0.8.26 finished in 36.26s\nCompiler run successful!\n","passed":true},{"durationMs":3821,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/RealV4Integration.t.sol:RealV4IntegrationTest\n[PASS] test_realV4MinimumOutputRollbackPreservesReserveAndPool() (gas: 576278)\n[PASS] test_realV4SwapSettlesDeltasCallsHookAndBurnsOnlyNewOutput() (gas: 952359)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.65ms (948.86µs CPU time)\n\nRan 12 tests for test/Accounts.t.sol:AccountsTest\n[PASS] test_counterfactualTokenDepositAndCrossAccountIsolation() (gas: 1074103)\n[PASS] test_depositSweepAndWithdrawAllAssets() (gas: 2476560)\n[PASS] test_falseReturningAndFeeTokensCannotInflateBalances() (gas: 804866)\n[PASS] test_onlyOwnerCanChangeOwnerWithNewOwnersSignatureAndNonce() (gas: 660996)\n[PASS] test_ownerChangeRedirectsOnlyToConsentingNewOwner() (gas: 682966)\n[PASS] test_reentrantAndRejectingOwnerCannotDrainOrLoseWithdrawal() (gas: 912838)\n[PASS] test_registrationAcceptsERC1271OwnerAndOperator() (gas: 1033057)\n[PASS] test_registrationExpiryDomainAndInputValidation() (gas: 217678)\n[PASS] test_registrationRequiresBothSignaturesAndCannotReplay() (gas: 288612)\n[PASS] test_tokenCallbackCannotReenterDeposit() (gas: 655998)\n[PASS] test_unsupportedAssetZeroAmountsUnknownAccountsAndUnlabelledETH() (gas: 218910)\n[PASS] test_withdrawOnlyOwnerWhilePausedAndFrozenAllAssets() (gas: 1378718)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 20.12ms (21.47ms CPU time)\n\nRan 2 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_factoryDeploymentEmptyChainFixedOwnerAndPredictableAddress() (gas: 14596198)\n[PASS] test_forwarderCreate2PredictionIncludesVaultAccountAndCreationCode() (gas: 11418)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 42.88ms (43.09ms CPU time)\n\nRan 11 tests for test/PrizesAndRoles.t.sol:PrizesAndRolesTest\n[PASS] testFuzz_prizeConservation(uint96,uint96,uint8) (runs: 256, μ: 821347, ~: 882650)\nLogs:\n  Bound result 2\n  Bound result 1\n\n[PASS] test_badProofsAndRootTotalsAreBoundedByReservation() (gas: 1566002)\n[PASS] test_claimsWhilePausedFrozenAndOwnerChanged() (gas: 1088722)\n[PASS] test_initialRolesAndOwnerAreFixedIndependentOfDeployer() (gas: 68631)\n[PASS] test_lastSecondClaimAndFullyClaimedExpiry() (gas: 835428)\n[PASS] test_pauseScopeAndOwnerCannotMoveOrRedirectPlayerFunds() (gas: 1145266)\n[PASS] test_postsClaimsAndExpiresOnlyUnclaimedAllAssets() (gas: 3466157)\n[PASS] test_prizePostingCannotReservePlayerMoneyBurnReserveOrOtherAsset() (gas: 1042689)\n[PASS] test_resultSignerRotationRejectsFormerSignerForExistingGames() (gas: 1040864)\n[PASS] test_roleChangesOnlyOwnerAndTakeFull48Hours() (gas: 605319)\n[PASS] test_roleReplacementRestartsDelayCancellationAndNonzeroValidation() (gas: 319338)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 127.75ms (48.37ms CPU time)\n\nRan 13 tests for test/ShopAndBurn.t.sol:ShopAndBurnTest\n[PASS] testFuzz_exactPurchaseHalves(uint96,uint8,bool) (runs: 256, μ: 504784, ~: 522924)\nLogs:\n  Bound result 84587169912387108379\n\n[PASS] test_burnMustCallbackOnceAndTransferOutput() (gas: 1033735)\n[PASS] test_burnOnlyKeeperReserveLimitsAndCallbackAuthentication() (gas: 634993)\n[PASS] test_burnSlippagePartialFillAndEmptyOutputRevertAtomically() (gas: 981912)\n[PASS] test_burnStillWorksPaused() (gas: 700285)\n[PASS] test_failedPurchaseRecipientTransferRollsBackEverything() (gas: 726619)\n[PASS] test_operatorShopAndGamesShareDailyCapAllAssets() (gas: 3135169)\n[PASS] test_ownerAndDirectPurchasesSplitExactlyAllAssets() (gas: 2163598)\n[PASS] test_ownerChangeInvalidatesOperatorPurchaseSignature() (gas: 654381)\n[PASS] test_purchaseInputAndAuthorityFailuresAndFrozenDirectPayer() (gas: 492898)\n[PASS] test_purchaseSignatureCannotChangeItemAmountOrAssetAndCanBeCancelled() (gas: 579176)\n[PASS] test_relayOwnerAndOperatorSignaturesAndReplayProtection() (gas: 1066248)\n[PASS] test_reserveBurnUsesExactPoolPaysETHAndBurnsAllSHIFT() (gas: 1012921)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 127.82ms (31.15ms CPU time)\n\nRan 14 tests for test/GamesAndCaps.t.sol:GamesAndCapsTest\n[PASS] testFuzz_feeConservesEveryWei(uint96,uint8,bool) (runs: 256, μ: 1121347, ~: 1243715)\nLogs:\n  Bound result 4587169912387108379\n\n[PASS] testFuzz_rollingWindowMatchesIndependentHistory(uint32[12],uint64[12]) (runs: 256, μ: 5805608, ~: 5835277)\n[PASS] test_capChangesLowerImmediatelyRaiseAfterDelayReplaceAndMixedChange() (gas: 496914)\n[PASS] test_dailyAndPerGameCapsAllAssets() (gas: 7202724)\n[PASS] test_exactRollingWindowDoesNotResetAtMidnightOrFirstSpendAnniversary() (gas: 2217794)\n[PASS] test_feeFailureRollsBackSettlementAndRefundRemainsAvailable() (gas: 1144415)\n[PASS] test_freezeImmediateUnfreezeDelayAndRefreezeCancel() (gas: 1163071)\n[PASS] test_gameResultsAndFivePercentFeeAllAssets() (gas: 9858389)\n[PASS] test_lockRejectsUnauthorizedDuplicateSameOwnerInvalidStakeAndExpiry() (gas: 1084890)\n[PASS] test_loweringCapsBelowExistingSpendDoesNotUnderflowOrEraseHistory() (gas: 1498498)\n[PASS] test_ownerChangePreservesSpendAndCancelsPendingRelaxation() (gas: 1605624)\n[PASS] test_refundBoundaryAndSettlementCutoffAllAssets() (gas: 3130336)\n[PASS] test_settleWorksPausedAndFrozen() (gas: 1323537)\n[PASS] test_wrongSignerExpiredSignatureWrongDomainInvalidResultAndDoubleSettlement() (gas: 1207701)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 127.85ms (206.77ms CPU time)\n\nRan 2 tests for test/VaultInvariant.t.sol:VaultInvariantTest\n[PASS]\nVaultInvariantTest invariants:\n[PASS] invariant_allLiabilitiesBackedAndExternalFlowsConserved\n[PASS] invariant_epochReservationsEqualUnclaimedPrizes\n[PASS] invariant_lockedBalancesEqualAllLiveGameStakes\n VaultInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭--------------+----------------+-------+---------+----------╮\n| Contract     | Selector       | Calls | Reverts | Discards |\n+============================================================+\n| VaultHandler | advanceTime    | 579   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | burn           | 593   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | buy            | 591   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | buyDirect      | 620   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | claimOrExpire  | 566   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | deposit        | 539   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | freeze         | 616   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | lock           | 531   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | pause          | 600   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | post           | 603   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | setCaps        | 634   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | settleOrRefund | 584   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | sweep          | 544   | 0       | 0        |\n|--------------+----------------+-------+---------+----------|\n| VaultHandler | withdraw       | 592   | 0       | 0        |\n╰--------------+----------------+-------+---------+----------╯\n\nLogs:\n  Bound result 86400\n  Bound result 9089\n  Bound result 12184\n  Bound result 14604\n  Bound result 55684\n  Bound result 2324\n  Bound result 13\n\n[PASS] test_handlerTraversesAllEconomicTransitions() (gas: 4273562)\nLogs:\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 10000000000000000\n  Bound result 1000000000000000000\n  Bound result 100000000000000000\n  Bound result 100000000000000000\n  Bound result 10000000000000000000\n  Bound result 1000000000000000000\n  Bound result 500000000000000000\n  Bound result 1000000000000000000\n  Bound result 2678400\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.67s (3.67s CPU time)\n\nRan 7 test suites in 3.67s (4.12s CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":84,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"DepositForwarder.receive()\",\"DepositForwarder.sweep(address)\",\"NightshiftVault.applyRoleChange(uint8)\",\"NightshiftVault.burnReserve(uint256,uint256)\",\"NightshiftVault.buy(bytes32,bytes32,address,uint256)\",\"NightshiftVault.buyDirect(bytes32,bytes32,address,uint256)\",\"NightshiftVault.buyWithSig(bytes32,bytes32,address,uint256,address,uint256,bytes)\",\"NightshiftVault.cancelRoleChange(uint8)\",\"NightshiftVault.claimPrize(uint256,bytes32,address,uint256,bytes32[])\",\"NightshiftVault.deposit(bytes32,address,uint256)\",\"NightshiftVault.depositETH(bytes32)\",\"NightshiftVault.expirePrizes(uint256,address)\",\"NightshiftVault.invalidatePurchaseNonce(bytes32,uint256)\",\"NightshiftVault.lockGame(bytes32,bytes32,bytes32,address,uint256,uint256)\",\"NightshiftVault.postPrizes(uint256,bytes32,address,uint256)\",\"NightshiftVault.receive()\",\"NightshiftVault.refundGame(bytes32)\",\"NightshiftVault.register(bytes32,address,uint256,bytes,bytes)\",\"NightshiftVault.scheduleRoleChange(uint8,address)\",\"NightshiftVault.setCaps(bytes32,address,uint256,uint256)\",\"NightshiftVault.setFrozen(bytes32,bool)\",\"NightshiftVault.setOwner(bytes32,address,uint256,bytes)\",\"NightshiftVault.setPaused(bool)\",\"NightshiftVault.settle(bytes32,uint8,uint256,bytes)\",\"NightshiftVault.sweep(bytes32,address)\",\"NightshiftVault.unlockCallback(bytes)\",\"NightshiftVault.withdraw(bytes32,address,uint256)\"],\"files\":{\".gitignore\":7,\"LICENSE\":23,\"README.md\":152,\"docs/DEPENDENCIES.md\":20,\"docs/INTEGRATION.md\":118,\"docs/SECURITY.md\":77,\"docs/VALIDATION.md\":29,\"foundry.toml\":23,\"launch.json\":10,\"remappings.txt\":4,\"src/DepositForwarder.sol\":48,\"src/NightshiftVault.sol\":804,\"src/interfaces/IV4PoolManager.sol\":28,\"test/Accounts.t.sol\":228,\"test/Deployment.t.sol\":67,\"test/GamesAndCaps.t.sol\":345,\"test/PrizesAndRoles.t.sol\":273,\"test/RealV4Integration.t.sol\":138,\"test/ShopAndBurn.t.sol\":245,\"test/VaultInvariant.t.sol\":357,\"test/VaultTestBase.sol\":178,\"test/mocks/MockAssets.sol\":107,\"test/mocks/MockV4PoolManager.sol\":88},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"eb29997f64513f333035176d5754ca4de058cc67a5315cd425152864237e3856","verifiedTreeHash":"28cd30d9b529871199132190d04b0f6e447772ce","verifierVersion":"0.1.0+a26ee00d"}]}