{"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":"2c9eb033-ae9f-4b2b-a887-f1cc368e1fcb","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"8f6e2fb485ea2d2bdca732e780a83e733062697e5b2b410412e086190291f57d","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":"e155026de4fc3e077c486c4ebc1e92ca6a41f3d1ef2786aa943dcfff85e4352e","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":"93b68141e7eb6baefa7d366c384fb79a5e08157c7a6961ab2808066947b75ec9","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":"acaa2242ae7b9437c8a3767d24a47bac68b36c3e14a4ba5522a7e03ed348b954","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":"fce7879fb8a22ca339c716900aaf3a189ba04339bda34c7bfdb2cc88ab2662ef","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":"73269e39124baf1c0d57b92465b0b9d3ec59fb75455ffc1e75b8f3d233b23932","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":"fb92d01b164e0ebdde6c25045a6960ff1c85099590a162ed5e274908b0c2e3f7","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"f4bab37b10c3ae2538b51cae6182e6d1a51ad10371ed2bccfe3fc85bc903066d","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":"Project site: https://hamsterhour.fun\n\nLaunch 1 of 2 for $HAMSTER: deploy HamsterVault on Ethereum mainnet (evm_contracts, no token yet). Launch 2 is a Uniswap v4 hook token HAMSTER (1B, 18 decimals) with remainderTo = this vault (exactly 800M); its hook sends IMD here. It must survive future game versions (Squeekchain will migrate data to mainnet).\n\nGame v2: 0xe33ab493249a2df0d8c4a71ac54f46d1f7778367 (github.com/identity-md-launches/launch-2260-build-hamster-hour-v2). IMD 0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7. TEAM = launch wallet. No owner, pause, upgrade or withdrawal path; TEAM powers are only those listed.\n\nBIND: bindToken(token), TEAM, once; requires totalSupply == 1e27 and balanceOf(vault) == 8e26. Buckets: mining 750M, boost 50M.\n\nGAMES: registry starting with v2. TEAM can scheduleGame(newGame) with a 48h timelock (public, no other effect); on execute it becomes currentGame. Each game must expose lossOf, totalEligibleLoss, cumulativeLossBefore/After, window times and finalization, the staking-recipient pull balance, depositJackpot and fundOracleReserve; check the interface on schedule. The previous game counts only windows starting before the switch; the new game's loss range starts after the previous game's last counted window is finalized, so ranges never overlap and halvings never reset. Old windows stay claimable forever. For 14 days after a switch, mining from the new game is capped at 2M HAMSTER per UTC day (deterministic scaling).\n\nMINING (pull, retroactive from the first v2 window): 20,000 HAMSTER per 1 IMD of eligible loss, halving every 18,750 IMD of global cumulative loss (converges to 750M). Window budget = F(after) - F(before) on the global curve; account share = budget * lossOf / totalEligibleLoss, rounded down, dust tracked. claimMined(game, windows[], beneficiary) is permissionless, pays only the beneficiary, once per (game, k, account), bounded. Committed emissions never exceed the bucket; claim order never changes amounts.\n\nVESTING: each claim creates a 7-day linear, non-transferable grant; unvested and vested-unwithdrawn balances count as stake weight.\n\nSTAKING (IMD rewards only):\n- stake/unstake HAMSTER. Weight earns after a 24h warm-up; requestUnstake stops weight now and releases after 24h; checkpoint on every change.\n- Sources: pull the 5% staker share from any registered game; depositRewards(amt) (hook 0.5%) streamed over 7 days; depositDrip(amt) (80% of anti-snipe tax) as a continuous 1%/day drip of the remainder; the game backlog streams over 30 days.\n- Strictly pro-rata over time. With zero weight, rewards wait: no first-staker windfall. Buckets never count as weight.\n\nFORWARDING: forwardJackpot(amt) and forwardOracle(amt) pull IMD from the caller (the hook) into separate balances that anyone can flush to currentGame's depositJackpot and fundOracleReserve. The hook never needs changing after a game switch.\n\nSQUEEK BOOST (50M, dormant): activateBoost(collection), TEAM once, 12h timelock, for the canonical Squeek NFT on Ethereum mainnet only. Holders register tokenId to their wallet without transfer; it counts for windows starting 24h+ after registering and is void if ownerOf changes. +50% of base mined, max 10k per NFT and 100k globally per UTC day (by window start), deterministic, same vesting. If not activated within 365 days, anyone can move the 50M into mining once.\n\nVIEWS for the site: claimable per game and window, grants, stake, warm-up, unstake queue, pending IMD, streams and drip, buckets left, rate and halving, games and pending switch.\n\nDeliver: Foundry, tests (retroactive claims, halving crossing, game switch with an overlapping backlog, the new-game cap, claim-order independence, bucket conservation, vesting as stake, warm-up, unstake, zero-weight periods, drip, forwarding after a switch, bind checks, boost voiding and caps, the 365-day sweep), ABI, README, real addresses only. After deploy TEAM calls scheduleStakingRecipient(vault) on the game.","parentJobId":null,"planHash":"1b32ca4909964c6e36fedba43bea3f3504417d00efbdf926ad9a6b7dd3c6f94e","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"2c9eb033-ae9f-4b2b-a887-f1cc368e1fcb","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2304-project-site-https-hamsterhour-fun-launc"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51232","feedbackHash":"ab889d4380f36ea6da7fd09e7e5f42dccc28e0a751d4e2f29c2418219629d840","nodeKey":"audit_economics","submissionHash":"8f6e2fb485ea2d2bdca732e780a83e733062697e5b2b410412e086190291f57d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52164","feedbackHash":"c48dea49851e60000b6dc2691478c6e12f814bb119076d5be18fd8037a5f1aae","nodeKey":"audit_flow","submissionHash":"e155026de4fc3e077c486c4ebc1e92ca6a41f3d1ef2786aa943dcfff85e4352e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50974","feedbackHash":"cdf0cb4b60727851670926a996c053475620671ecc88ac87bbe5e1781b04b04a","nodeKey":"audit_judge","submissionHash":"93b68141e7eb6baefa7d366c384fb79a5e08157c7a6961ab2808066947b75ec9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51291","feedbackHash":"9a54c818a4d9d06ef1d59b0576ce481b116fdb34cb5eb3d86844c2a3547c6632","nodeKey":"audit_judge","submissionHash":"a4f8f9b6ba7216098b57e81799ef0d2b9913a76d70b9c6a8f4ca1d28b24cb367","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51419","feedbackHash":"64a9a4b2bb077d2506b738c7a525deaef0197260f95ece296641f5b733a11266","nodeKey":"audit_judge","submissionHash":"408afbe3051df77fbf24bb37411a10f3c09298ae423dadfb49e022b4e147521c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52252","feedbackHash":"472985331347170b3a1c12817c74ec6b7605367a089af229be168fabfc3776c9","nodeKey":"audit_math","submissionHash":"acaa2242ae7b9437c8a3767d24a47bac68b36c3e14a4ba5522a7e03ed348b954","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52082","feedbackHash":"b7cc0aa2d1b7cb86d79d2902e2b4a2a7a7c5a4a1940d3c355126a80b807ec3ba","nodeKey":"audit_permissions","submissionHash":"fce7879fb8a22ca339c716900aaf3a189ba04339bda34c7bfdb2cc88ab2662ef","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52141","feedbackHash":"d243a0a405f5f10a5f68ba65f24659469b2e9cab17ecacd3ca543d5e382a5b40","nodeKey":"build_contract_project","submissionHash":"a2384c73a15534f57d4ee05aabf0e5f648cc2ee6fae23929cfb5ad8e8fb2759f","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51340","feedbackHash":"7599c979f80b572ff2e58c78f2fedc912a6c7dc0091f6c8888f74bc805618976","nodeKey":"build_contract_project","submissionHash":"88b19dd1c10a00b6f71ab142172bb3bd52000f82f5b86a1337d2ce15c25b7e67","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52193","feedbackHash":"2360b610e40f5469c445fbd7a246712f19ac9b0a4d85d70e89e464c8ebf4a09f","nodeKey":"build_contract_project","submissionHash":"73269e39124baf1c0d57b92465b0b9d3ec59fb75455ffc1e75b8f3d233b23932","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52251","feedbackHash":"4efe58c0138b948cb17a02c96dbd55bed45601a62b3c664b17575eaa542b3652","nodeKey":"manifest","submissionHash":"b3847826751c8593052fd1c0687368b49371ce40da6a28ddc2455d48abbba244","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51304","feedbackHash":"affc6527fc8bca74629926dd23844eb417a4c2ff5747298f8efb84ef5340a5de","nodeKey":"manifest","submissionHash":"a9f3e3dc0dbcd5624930372868cc58b4a482207d5303aec5ccdfefb5c11f1969","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50904","feedbackHash":"4efa53832be938faadd7d334c8b5506a84a93119ae772635b22aa8c49f1ddc5d","nodeKey":"manifest","submissionHash":"fb92d01b164e0ebdde6c25045a6960ff1c85099590a162ed5e274908b0c2e3f7","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52611","feedbackHash":"1d1bd6714df35bcace2880d7de9838958184b914dacb9edd69831de50a3ec631","nodeKey":"write_foundry_tests","submissionHash":"f4bab37b10c3ae2538b51cae6182e6d1a51ad10371ed2bccfe3fc85bc903066d","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52192","feedbackHash":"8f9ebeef61dd5a3240c4850ce54d269da7664ad887e86d4b8125ec7165546b52","nodeKey":"write_foundry_tests","submissionHash":"823ee0cd9c1b8e066c7b26e6da5e19cf55cc3db1739bd148e462e96aaa29d5d3","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52701","feedbackHash":"77cde354d5537223c02de230e814bbe854acc3ba5368cffa35ff700a6c845fd4","nodeKey":"write_foundry_tests","submissionHash":"5277909933f0d87922a40678e5f18112b1698a38c99846d99218a888d84902ed","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"02b695b06ee65dcc7a57e1476e6b73022fa782c3bd1d59d166d95341d6cd707b","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fea57d3e9d0ca7bf","findings":[{"citation":"resolved","description":"bindToken is the only path that sets `token`, and claimMined, claimBoost, stake, withdrawVested and withdrawUnstaked all revert NotBound until it succeeds. It reverts InvalidAmount unless the vault's HAMSTER balance is exactly 800_000_000e18. Launch 2 mints 1B, sends 800M to this vault and makes the rest tradable in the same transaction, while TEAM's bindToken is a later, separate transaction. Any holder of 1 wei of HAMSTER can transfer it to the vault in that gap (or front-run TEAM's bind in the mempool). Nothing can ever move HAMSTER out of an unbound vault and the contract has no owner, rescue or sweep path, so after such a transfer every bindToken call reverts forever: the 800M remainder, mining, vesting, staking and the boost bucket are permanently dead, and IMD pulled by depositRewards/depositDrip/pullGameRewards streams to zero weight forever. The vault already treats post-bind donations as unrescuable excess (hamsterLiability is computed from the 800M constant), so accepting balanceOf(vault) >= 800M at bind is consistent with the rest of the design; the brief's literal `== 8e26` should be confirmed with the requester as `>=`. Merged from audit_flow, audit_permissions and audit_economics (same root cause).","line":56,"path":"src/VaultBase.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\n/// @dev Minimal 18-decimal ERC-20 standing in for the launch-2 HAMSTER token.\ncontract Hamster {\n    string public constant name = \"HAMSTER\";\n    uint8 public constant decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @notice Finding: any HAMSTER holder can permanently block TEAM's one-shot bindToken by sending\n/// 1 wei of HAMSTER to the vault before TEAM binds. bindToken requires balanceOf(vault) == 8e26\n/// exactly, there is no rescue path for donations, and nothing else in the vault can ever set\n/// `token`, so the 800M allocation, mining, staking and boost claims are bricked forever.\ncontract BindGriefTest is Test {\n    address internal constant TEAM = address(bytes20(hex\"6cba4751742f6989fa224b2dc5d5d52fcef98a34\"));\n    HamsterVault internal vault;\n    Hamster internal hamster;\n    address internal sniper = makeAddr(\"sniper\");\n\n    function setUp() public {\n        vm.chainId(1);\n        HamsterBoost boost = new HamsterBoost();\n        vault = new HamsterVault(TEAM, address(boost));\n        hamster = new Hamster();\n        // Launch 2: 1B supply, exactly 800M to the vault, 200M to the pool (here: a buyer).\n        hamster.mint(address(vault), 800_000_000 ether);\n        hamster.mint(sniper, 200_000_000 ether);\n    }\n\n    function test_oneWeiDonationBeforeBindBricksVaultForever() public {\n        // A trader who bought any HAMSTER front-runs TEAM's bindToken with a 1 wei transfer.\n        vm.prank(sniper);\n        hamster.transfer(address(vault), 1);\n\n        // The honest launch state: correct token, correct supply, the full 800M delivered.\n        assertEq(hamster.totalSupply(), 1_000_000_000 ether);\n        assertGe(hamster.balanceOf(address(vault)), 800_000_000 ether);\n\n        // Expected: TEAM can still bind the genuine token (excess is a donation, as everywhere\n        // else in the vault). Actual: bindToken reverts InvalidAmount, now and forever.\n        vm.prank(TEAM);\n        vault.bindToken(address(hamster));\n        assertEq(vault.token(), address(hamster), \"vault must bind the genuine launch-2 token\");\n    }\n}","reproduction":"State: HamsterBoost and HamsterVault(TEAM, boost) deployed on chainId 1; an 18-decimal token with totalSupply 1e27, 800_000_000e18 held by the vault and 200_000_000e18 held by a trader. Steps: (1) trader calls token.transfer(vault, 1); (2) TEAM calls vault.bindToken(token). Expected: bind succeeds and vault.token() == token (the full 800M was delivered; the extra wei is a donation). Actual: revert InvalidAmount() at src/VaultBase.sol:54-57 because balanceOf(vault) == 800_000_000e18 + 1, and every later bindToken reverts identically since no function can lower the balance. Proof test/scratch/P1.t.sol fails on this tree with [FAIL: InvalidAmount()] and passes once the balance check becomes >= 800_000_000 ether.","severity":"critical","snippet":"                || IToken(token_).balanceOf(address(this)) != 800_000_000 ether || IToken(token_).decimals() != 18","title":"bindToken requires balanceOf(vault) == 800M exactly, so 1 wei of HAMSTER sent to the vault before TEAM binds locks the 800M allocation and all HAMSTER paths forever"},{"citation":"resolved","description":"The brief requires that if the boost is not activated within 365 days anyone can move the 50M into mining once. sweepDormantBoost marks the boost swept and adds 50M to the storage variable miningAllocation, but no emission path reads that variable: every window budget is F(after)-F(before) on HamsterBoost.emissionCurve, which saturates at the constant MINING_ALLOCATION = 750M (`if (h >= 90) return MINING_ALLOCATION;`), and HamsterMining._prepareWindow (src/HamsterMining.sol:157 `if (miningCommitted > MINING_ALLOCATION) revert BadLossHistory();`) still caps commitments at the same constant. So after the sweep the sum of all window budgets over any future loss history is still at most 750M; the 50M has no claim, grant, stake or rescue path and stays in the immutable vault forever, while bucketsLeft().miningUncommitted reports it as mineable (overstating supply by 50M) and hamsterLiability carries it as a reserve with no exit. README and docs/REVIEW.md document this as an interpretation, but the result is a permanent lock of 5% of supply that contradicts the requested behaviour. The fix is a scope decision for the requester: either make the curve converge to the live miningAllocation after the sweep (e.g. scale the remaining bands by 800/750 from the sweep's global-loss point so halvings never reset) and use miningAllocation in the commit check, or drop the sweep and stop the views reporting unreachable supply. Merged from audit_math, write_foundry_tests, audit_flow, audit_permissions and audit_economics (same root cause).","line":57,"path":"src/HamsterVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\ncontract SweepToken {\n    uint256 public totalSupply;\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract SweepGame {\n    address public constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    uint64 public constant genesis = 1791847200;\n    uint256 public nextFinalizeWindow;\n    uint256 public cumulativeLoss;\n    mapping(uint256 => mapping(address => uint256)) public lossOf;\n    mapping(uint256 => uint256) public totalEligibleLoss;\n    mapping(uint256 => uint256) public cumulativeLossBefore;\n    mapping(uint256 => uint256) public cumulativeLossAfter;\n    mapping(address => uint256) public balances;\n\n    function windowStart(uint256 k) external pure returns (uint64) {\n        return uint64(genesis + k * 1 hours);\n    }\n\n    function windowEnd(uint256 k) public pure returns (uint64) {\n        return uint64(genesis + (k + 1) * 1 hours);\n    }\n\n    function setLoss(uint256 k, address who, uint256 amount) external {\n        totalEligibleLoss[k] = totalEligibleLoss[k] - lossOf[k][who] + amount;\n        lossOf[k][who] = amount;\n    }\n\n    function finalize(uint256 maxWindows) external {\n        for (uint256 i; i < maxWindows; ++i) {\n            uint256 k = nextFinalizeWindow;\n            if (windowEnd(k) > block.timestamp) break;\n            cumulativeLossBefore[k] = cumulativeLoss;\n            cumulativeLoss += totalEligibleLoss[k];\n            cumulativeLossAfter[k] = cumulativeLoss;\n            ++nextFinalizeWindow;\n        }\n    }\n\n    function withdraw() external {}\n    function depositJackpot(uint256) external {}\n    function fundOracleReserve(uint256) external {}\n}\n\n/// @notice After sweepDormantBoost the 50M boost bucket is \"moved into mining\", but the emission\n/// curve still saturates at 750M, so the 50M can never be committed to any window: it is locked\n/// forever in an immutable contract with no rescue path.\ncontract SweepLockTest is Test {\n    address internal constant TEAM = address(0xBEEF);\n    address internal constant V2 = 0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367;\n    uint256 internal constant GENESIS = 1791847200;\n\n    function test_sweptBoostBucketMustBeMinable() public {\n        vm.chainId(1);\n        vm.warp(GENESIS + 2 hours);\n        SweepGame template = new SweepGame();\n        vm.etch(V2, address(template).code);\n        SweepGame game = SweepGame(V2);\n        HamsterBoost boost = new HamsterBoost();\n        HamsterVault vault = new HamsterVault(TEAM, address(boost));\n        SweepToken token = new SweepToken();\n        token.mint(address(vault), 800_000_000 ether);\n        token.mint(address(this), 200_000_000 ether);\n        vm.prank(TEAM);\n        vault.bindToken(address(token));\n\n        // Boost never activated: after 365 days anyone sweeps the 50M into mining.\n        vm.warp(GENESIS + 2 hours + 365 days);\n        vault.sweepDormantBoost();\n        assertEq(vault.miningAllocation(), 800_000_000 ether);\n\n        // Saturate the curve: 2,000,000 IMD of eligible loss in window 0 (h >= 90 halvings).\n        address alice = makeAddr(\"alice\");\n        game.setLoss(0, alice, 2_000_000 ether);\n        game.finalize(1);\n        uint256[] memory windows = new uint256[](1);\n        windows[0] = 0;\n        vault.claimMined(V2, windows, alice);\n\n        // Everything the curve can ever emit has now been committed. The swept 50M must be part of\n        // what mining can emit; currently it is stranded as an uncommittable \"reserve\".\n        (uint256 miningUncommitted,,,) = vault.bucketsLeft();\n        assertLt(miningUncommitted, 1 ether, \"swept 50M is not minable\");\n    }\n}","reproduction":"State: vault deployed at GENESIS+2h on chainId 1, token bound, v2 at 0xe33a... mocked with the IHamsterGame ABI, boost never activated. Steps: (1) warp to deployedAt + 365 days and call sweepDormantBoost(): miningAllocation() == 800_000_000e18 and bucketsLeft().miningUncommitted == 800_000_000e18; (2) give v2 window 0 an eligible loss of 2,000,000 IMD (h >= 90, curve saturated) and finalize it; (3) claimMined(V2, [0], alice). Expected: the swept 50M is part of what mining can emit, so after saturating the curve miningUncommitted approaches 0 (miningCommitted can reach 800M). Actual: miningCommitted == 750_000_000e18, emissionCurve(type(uint256).max) == 750_000_000e18, bucketsLeft().miningUncommitted == 50_000_000e18, and every later window has budget F(after)-F(before) == 0, so the 50M can never be committed. Proof test/scratch/P2.t.sol fails on this tree with [FAIL: swept 50M is not minable: 50000000000000000000000000 >= 1000000000000000000].","severity":"high","snippet":"        miningAllocation += BOOST_ALLOCATION;","title":"sweepDormantBoost only raises the miningAllocation counter: the emission curve and the commit cap stay at 750M, so the swept 50M can never be mined and is locked forever"},{"citation":"resolved","description":"The brief states that unvested and vested-unwithdrawn grant balances count as stake weight, i.e. the grantee decides when to pull vested principal out of the weight pool. withdrawVested(account, ids) can be called by anyone for any account; it pays the account but _removeLot subtracts the withdrawn amount from _reward.weight and accounts[account].weight (or from the pending activation during warm-up). stake() and requestUnstake() act only on msg.sender, but this economically equivalent weight-removal path is open to everyone. A competing staker can push every other grantee's vested slice out of the pool as soon as it vests (linear vesting vests every second, so this repeats each block for the full 7 days of every grant), raising the attacker's pro-rata share of every IMD stream and drip. The victim can only restore the weight by calling stake() again, paying gas and a fresh 24h warm-up during which the amount earns nothing, after which the attacker repeats the push. With two equal participants (victim grant G, attacker stake G) the victim's share drops from 50% to 6/13 after day one and keeps falling over the week. Minimal fix preserving the design: require msg.sender == account in withdrawVested (relaying withdrawUnstaked and claimRewards is harmless since neither touches weight), or let the account opt in to relayers. Merged from audit_math and audit_permissions.","line":288,"path":"src/HamsterStaking.sol","reproduction":"State (test/scratch/Repro.t.sol test_thirdPartyStripsVestedWeight, run and observed): bound vault, v2 window 0 finalized with lossOf(0, alice) = totalEligibleLoss = 100e18. Steps: (1) anyone calls claimMined(V2, [0], alice): alice receives a 7-day grant of 2_000_000e18 HAMSTER in lot 0; (2) warp +1 day, checkpoint(128), checkpointAccount(alice, 128): accounts(alice).weight == 2_000_000e18; (3) carol (unrelated) calls withdrawVested(alice, [0]). Expected: revert, alice's weight unchanged at 2_000_000e18. Actual: the call succeeds, 285_714.28e18 HAMSTER (1/7 of the grant) is pushed to alice's wallet, accounts(alice).weight drops to 1_714_285.71e18 and _reward.weight drops by the same amount, so every other staker's share of all streams rises; alice can only regain the weight by staking again and waiting 24h.","severity":"medium","snippet":"    function withdrawVested(address account, uint256[] calldata ids) external guarded returns (uint256 amount) {","title":"withdrawVested has no caller check, so anyone can strip another account's vested-unwithdrawn stake weight every block and force it through a new 24h warm-up"},{"citation":"resolved","description":"For windows starting within 14 days of a switch, windowBudget scales each window of a UTC day by 2M/sum. The scaled value is what _prepareWindow commits (miningCommitted += budget), but the curve position still advances by the full raw amount: the next window's F(before) equals this window's F(after), computed from cumulative loss, not from what was paid. The withheld difference (raw - scaled) is never emitted by any later window and has no other path out of the vault, so it is permanently locked exactly like the swept boost reserve. The brief describes the cap as a rate limit ('capped at 2M HAMSTER per UTC day, deterministic scaling') on a curve that 'converges to 750M'; the implementation turns it into a destruction of up to 48M - 2M = 46M per day at the initial rate, i.e. up to 644M of the 750M over a busy 14-day migration (including the planned Squeekchain migration), decided by player activity in the successor's first two weeks. bucketsLeft().miningUncommitted then overstates mineable supply by the stranded amount forever. README documents 'withheld amounts do not restart or shift the curve', so this needs a scope decision by the requester: carry the withheld amount forward (a deferred pool emitted after the cap period), or advance the curve only by what is actually committed. The arithmetic of the scaling itself (floor, sum <= 2M, order independence, day and 14-day bounds by window start) was checked and holds. Merged from audit_math and audit_economics.","line":444,"path":"src/HamsterBoost.sol","reproduction":"State (test/scratch/Repro.t.sol test_capStrands, run and observed): bound vault; TEAM scheduleGame(v3 with the same genesis), warp 48h, executeGame(); on the first full UTC day after the switch v3 has 24 windows with 100 IMD of eligible loss each (2,400 IMD, h = 0, rate 20,000 HAMSTER per IMD, raw budget 48,000,000). After the day ends both games finalize, resolveGame(v3), then claimMined(v3, the 24 windows, alice). Expected: 2M paid now and the remaining 46M of that day's curve still emittable later. Actual: granted 1,999,999,999,999,999,999,999,992 (the cap); currentMiningState().globalLoss == 2,400e18 and emissionCurve(2,400e18) == 48,000,000e18 consumed; miningCommitted == 2M; bucketsLeft().miningUncommitted == 748,000,000e18 but the maximum future emission 750M - F(globalLoss) == 702,000,000e18, so 46M is unreachable forever.","severity":"medium","snippet":"        if (sum > NEW_GAME_DAILY_CAP) budget = M.mulDiv(budget, NEW_GAME_DAILY_CAP, sum);","title":"New-game daily cap forfeits the withheld emission instead of deferring it: the global curve advances by the raw amount while only the scaled budget is committed, stranding up to 46M per capped day"},{"citation":"resolved","description":"Every depositRewards call creates a hook stream end at clock + 7 days; with nonzero weight the clock advances one per second, so deposits in distinct seconds create distinct events (merging only happens for an identical finish). Every stake or grant creates an activation 24h later at its own second. _readyGlobal processes at most MAX_CHECKPOINT = 128 events and reverts when more remain, and depositRewards/depositDrip call it before accepting funds. This happens organically, not only by attack: a launch-day burst of N swaps at distinct seconds produces N stream ends that all mature exactly 7 active days later within the same span; if the pool is then quiet for longer than 128 of those matured events, the next depositRewards/depositDrip reverts, and keeps reverting for every swap until someone calls checkpoint(128) ceil((N-128)/128) times. If the launch-2 hook calls these inline from afterSwap without try/catch, swaps on the HAMSTER pool revert for that period; the brief says the hook 'sends IMD here' and should never need changing. An attacker can also force it for about 33M gas by staking 1 wei in 129 consecutive blocks. Recovery is permissionless and cheap, so funds are safe. A fix that keeps the design: let depositRewards/depositDrip append their stream end and drip anchor at the stored clock without requiring the global state to be current (the new finish = storedClock + 7d is >= every queued finish, so ordering is preserved), or document for the hook author that these calls must be wrapped. Merged from audit_flow and audit_economics.","line":206,"path":"src/HamsterStaking.sol","reproduction":"State (test/scratch/Repro.t.sol test_checkpointLivenessOrganic, run and observed): bound vault; alice stakes 1e18 and the warm-up matures so the reward clock runs. Steps: (1) 129 depositRewards(1e18) calls 12 seconds apart (a 26-minute burst); (2) warp +7 days + 1 hour with no vault transaction (a quiet pool); (3) call depositRewards(1e18). Expected: the deposit is accepted. Actual: revert CheckpointRequired(); depositDrip(1e18) and the pendingIMD view revert the same way; checkpoint(128) returns false, a second checkpoint(128) returns true, and only then does depositRewards succeed.","severity":"medium","snippet":"        if (!_checkpoint(MAX_CHECKPOINT)) revert CheckpointRequired();","title":"depositRewards and depositDrip revert CheckpointRequired after more than 128 unattended events, so a burst of hook deposits followed by a quiet period a week later makes the hook's swap calls fail unt"},{"citation":"resolved","description":"scheduleGame lets TEAM replace a pending candidate and restart the 48h delay, so its public timelock can lead to a correction. schedule() for the boost reverts AlreadyDone while a pendingCollection exists and execute() is permissionless after 12h, so once TEAM commits a wrong address (a typo or a non-canonical Squeek clone that passes supportsInterface) there is no path to the right one: after execute the collection is fixed forever, activateBoost(correct) reverts AlreadyDone and sweepDormantBoost is impossible too (sweep requires collection == 0). The one-shot property should bind at execute, not at schedule: allow TEAM to overwrite pendingCollection and restart readyAt before execution. Precondition is a TEAM mistake or compromised key, hence low. From audit_permissions.","line":139,"path":"src/HamsterBoost.sol","reproduction":"State (test/scratch/Repro.t.sol test_boostNotReschedulable, run and observed): chainId 1, two ERC-721 mocks W (wrong) and S (canonical) both returning true for supportsInterface(0x80ac58cd). Steps: (1) TEAM calls vault.activateBoost(W): Scheduled; (2) TEAM calls vault.activateBoost(S). Expected: the pending commitment is replaced by S and the 12h delay restarts, as scheduleGame does. Actual: revert AlreadyDone() at src/HamsterBoost.sol:139; (3) after 12h anyone calls executeBoost() and boost.status(vault).collection == W permanently.","severity":"low","snippet":"        if (s.collection != address(0) || s.pendingCollection != address(0) || s.swept) revert AlreadyDone();","title":"activateBoost commitment cannot be replaced or cancelled during its 12h timelock, unlike scheduleGame, so a wrong collection address binds the 50M boost bucket permanently"},{"citation":"resolved","description":"executeGame registers the successor with rangeReady = false and makes it currentGame immediately. currentMiningState(vault) reads games(currentGame) and reverts while the range is unresolved. Resolution needs the previous game finalized up to its end (its last counted window ends up to 1h after the switch and real v2 waits up to TIMEOUT = 48h for an unsettled window) and the successor finalized up to its first counted window, so the view the brief requires ('rate and halving') reverts instead of reporting the last known global loss. The data is available: the previous game's finalized prefix plus its globalOrigin give the current global loss, and the view can fall back to the previous game while rangeReady is false. From audit_flow.","line":385,"path":"src/HamsterBoost.sol","reproduction":"State (test/scratch/Repro.t.sol test_miningStateRevertsAfterSwitch, run and observed): v2 current, successor game scheduled by TEAM. Steps: (1) warp 48h and call executeGame(); (2) call vault.currentMiningState() in the same block. Expected: (globalLoss, rate, halving) from the finalized history so far. Actual: revert NotReady() until resolveGame(successor) succeeds, which requires v2.nextFinalizeWindow() >= games(v2).end and successor.nextFinalizeWindow() >= games(successor).first.","severity":"low","snippet":"        if (!g.rangeReady) revert NotReady();","title":"currentMiningState() reverts NotReady from executeGame until resolveGame succeeds, so the site's rate/halving view is unavailable for hours to days after every switch"},{"citation":"resolved","description":"prepare() walks registeredTokens (which never shrinks; voided tokens keep their slot) twice per UTC day, at most 8 registrations per call. Pass one writes a fresh eligibleCount slot per eligible registration per window (up to 25 windows, ~22k gas each); pass two makes one lossOf external call plus a mulDiv per window. Days are strictly sequential (s.nextDay) and claims for a day need it complete, and nobody is paid for preparing. audit_flow measured with the repository mocks: 64 registrations with losses in all 25 windows need 16 prepareBoostDay(8) calls and 36.5M gas for one day (~570k gas per registration per day); with 10,000 registered tokens a day needs 2,500 calls. Any holder can inflate the count by registering tokens that will never be eligible. If preparers stop, every later boost day is unclaimable although the brief requires boost mining to be deterministic and claimable. Not a loss of funds. Consider snapshotting per-wallet eligible counts once per day instead of per window, bounding work by wallets rather than NFTs, or raising the per-call limit. Merged from audit_flow and audit_economics.","line":256,"path":"src/HamsterBoost.sol","reproduction":"State: boost activated with a mock ERC-721; 64 holders each register one tokenId; every holder has lossOf = 1e18 in all 25 windows of the first prepared day. Steps: (1) warp to the end of that day + 3h, game.finalize(20000); (2) call vault.prepareBoostDay(8) until days_(vault, day).complete. Expected: a per-day cost one volunteer can sustain. Actual (audit_flow's measurement, consistent with the two-pass loop at src/HamsterBoost.sol:262-308): 16 calls and 36,511,523 gas for that single day, growing linearly in registered NFTs times windows per day; with 10,000 registrations 2,500 calls per day and claimBoost(day+1, id) reverts NotReady until the backlog is worked through.","severity":"low","snippet":"        if (maxRegistrations == 0 || maxRegistrations > 8) revert TooMany();","title":"Boost day preparation enumerates every NFT ever registered at most 8 per call with per-window storage writes and lossOf calls, so large collections cost hundreds of unrewarded calls per day and boost "},{"citation":"resolved","description":"For an uncached window inside the 14-day cap, windowBudget loops over every window of that UTC day and calls _rawBudget for each (about 8 external staticcalls plus two emissionCurve evaluations per window). The day sum is never cached, so each of the day's 24 windows repeats the same 24 _rawBudget calls on first claim, and HamsterBoost._startDay (line 243) repeats it again for every window when the boost day is prepared. audit_math measured against the mocks: claimMined of one capped window 1,006,407 gas; 23 capped windows in one call 10,390,165 gas; the first prepareBoostDay(8) on a capped day with 24 loss-bearing windows 11,371,955 gas before any registration work. This fits in a block, so it is a cost and liveness concern (14 such boost days cost ~160M gas of unpaid maintenance), not a brick. Caching the day sum in windowAccounting on first computation removes the quadratic factor. From audit_math; consistent with my own run of test_capStrands, where claiming the 24 capped windows of one day cost 16.4M gas.","line":441,"path":"src/HamsterBoost.sol","reproduction":"State: as test/scratch/Repro.t.sol test_capStrands (v3 switched in, 24 windows with 100 IMD each on the first full UTC day, both games finalized, v3 resolved). Steps: measure gas around vault.claimMined(v3, the 24 windows, alice). Expected: cost linear in the number of windows claimed (a few hundred k gas). Actual: 16,444,874 gas for the single call in my run (each window re-reads all 24 sibling raw budgets); audit_math measured 1,006,407 for one window and 11,371,955 for the first prepareBoostDay(8) on that day.","severity":"low","snippet":"            (uint256 raw,) = _rawBudget(vault, game, i);","title":"Capped-period windowBudget recomputes every sibling raw budget of the UTC day on each call, so a capped window costs ~1M gas per claim and ~11M gas for the day's boost preparation"},{"citation":"resolved","description":"executeGame resolves the outgoing game's range before switching, and _resolveGame(game) requires IHamsterGame(game).nextFinalizeWindow() >= g.first (src/HamsterMining.sol:130). If the current successor is a contract whose finalize stalls (a bug, or an oracle-dependent finalize without v2's permissionless 48h timeout), its range can never be resolved, so executeGame for any further candidate reverts NotFinalized forever: the registry is stuck on the dead game, mining from it is impossible (windowBudget reverts NotReady while rangeReady is false), and flushJackpot/flushOracle keep sending forwarded IMD to it. TEAM selects games and README documents registered games as trusted, and checkGame probes only the ABI, so this is a consequence of a TEAM mistake rather than a permission bypass; it is reported because the brief requires the vault to survive future game versions and there is no path out (no cancel, no forced resolution). A fix that keeps the design: let executeGame proceed when the outgoing game has no finalized window at or past its first (treat its counted loss as zero and chain the origins from its predecessor), or require candidates to expose a permissionless timeout the way v2 does.","line":103,"path":"src/HamsterMining.sol","reproduction":"State (test/scratch/Stuck.t.sol test_stalledSuccessorBlocksEverySwitch, run and observed): bound vault, v2 current. Steps: (1) TEAM scheduleGame(v3), warp 48h, executeGame(): v3 is current with rangeReady = false; (2) v3 never calls finalize (nextFinalizeWindow stays 0 < games(v3).first) while v2 finalizes normally; warp 10 days; resolveGame(v3) reverts NotFinalized; (3) TEAM scheduleGame(v4), warp 48h, executeGame(). Expected: the registry can move on to v4 (the brief requires surviving future game versions). Actual: revert NotFinalized() from _resolveGame(v3) inside executeGame, currentGame stays v3, and this repeats for every future candidate.","severity":"low","snippet":"        if (!games[old].rangeReady) _resolveGame(old);","title":"A successor game whose finalization never advances past its first counted window blocks resolveGame and every later executeGame forever, with no override"}],"hash":"408afbe3051df77fbf24bb37411a10f3c09298ae423dadfb49e022b4e147521c","nodeId":"c6642f56-f20b-4acf-a70b-97ec790c8d0c","outcome":"completed","summary":"The review is complete. No tracked file changed; the findings are in `.imd-findings.json` and the scratch tests live only under the ignored `test/scratch/` directory.\n\n**Outcome.** I kept 10 findings, each reproduced against this tree by a Foundry run, and answered all 33 verifier entry points plus 3 invariant rows. The existing 72-test suite passes and the vault's runtime size is under the limit, so the factory deployment model is not a blocker.\n\n**Merged specialist findings, by severity:**\n\n1. **Critical, bind dust lock.** bindToken requires the vault's balance to equal 800M exactly. A 1 wei transfer from anyone before TEAM binds makes every bind revert forever, with no HAMSTER outflow from an unbound vault. Three specialists reported it; their proof fails here with `InvalidAmount()` and is attached.\n2. **High, swept 50M unmineable.** sweepDormantBoost raises only a counter. The curve and the commit cap stay at 750M, so the 50M is locked permanently and the bucket view overstates supply. All five specialists reported it; the attached proof fails here.\n3. **Medium, vested weight stripping.** withdrawVested has no caller check. A third party removed one seventh of a grantee's weight after one day in my run, forcing a fresh 24h warm-up to recover it.\n4. **Medium, cap strands emission.** During the 14-day cap the curve advances by raw loss while only the scaled 2M is committed. One 2,400 IMD day stranded 46M in my run.\n5. **Medium, hook deposits revert after 128 unattended events.** I reproduced it organically: 129 hook deposits at distinct seconds, then a quiet hour a week later, and depositRewards and depositDrip revert until a keeper calls checkpoint twice.\n6. **Low.** Boost commitment cannot be rescheduled; the rate view reverts between executeGame and resolveGame; two separate gas findings on boost day preparation and capped-window recomputation.\n\n**My own addition (low).** A successor game whose finalization never reaches its first counted window blocks resolveGame and every later executeGame permanently. The registry would be stuck on the dead game with forwarded IMD still flushing to it. It is a trusted-game failure, so I rated it low.\n\n**Dropped or downgraded.** The specialists' trust-assumption note and untested-edge note were informational, not defects, and were not kept. All slither and aderyn lines were either false positives (the mulDiv XOR, the modulo \"PRNG\", guarded reentrancy) or already covered above.","treeHash":null,"usage":{"cachedInputTokens":1526418,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":45625,"runtime":"claude","turns":27,"wallClockMs":789564}},{"artifacts":[],"attempt":1,"bundleHash":"3b0b93f0fcacc86377d604a75e7b5c0b3e35d8a529a14605c15021237e5e6533","device":"aac006f7fc669006","findings":[{"description":"The brief says: 'If not activated within 365 days, anyone can move the 50M into mining once.' sweepDormantBoost() only increments the storage variable miningAllocation (750M -> 800M). Nothing reads miningAllocation when emitting: window budgets come from HamsterBoost.emissionCurve, which saturates at the constant MINING_ALLOCATION (750M), and HamsterMining._prepareWindow caps miningCommitted against the same constant (src/HamsterMining.sol:157). So after the sweep no amount of loss can ever commit more than 750M, while bucketsLeft() reports miningUncommitted = 50M permanently. The 50M HAMSTER has no other outflow (no admin path by design), so it is locked in the vault forever and the site view advertises a mining bucket that can never pay. docs/REVIEW.md acknowledges no new curve was defined; the brief's behaviour is therefore unimplemented rather than an explicit design choice the requester approved. Fix options for the requester to choose: extend the curve's asymptote to miningAllocation (e.g. scale the post-sweep tail), or define how the swept 50M is emitted.","line":60,"path":"src/HamsterVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\ncontract Tok {\n    uint256 public totalSupply;\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    function mint(address to, uint256 a) external { totalSupply += a; balanceOf[to] += a; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n}\n\ncontract Game {\n    uint64 constant G = 1791847200;\n    uint256 public nextFinalizeWindow = 2;\n    function IMD() external pure returns (address) { return 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7; }\n    function windowStart(uint256 k) external pure returns (uint64) { return uint64(G + k * 1 hours); }\n    function windowEnd(uint256 k) external pure returns (uint64) { return uint64(G + (k + 1) * 1 hours); }\n    function lossOf(uint256 k, address) external pure returns (uint256) { return k == 0 ? 100 * 18_750 ether : 0; }\n    function totalEligibleLoss(uint256 k) external pure returns (uint256) { return k == 0 ? 100 * 18_750 ether : 0; }\n    function cumulativeLossBefore(uint256 k) external pure returns (uint256) { return k == 0 ? 0 : 100 * 18_750 ether; }\n    function cumulativeLossAfter(uint256) external pure returns (uint256) { return 100 * 18_750 ether; }\n}\n\n/// After the 365-day sweep \"moves the 50M into mining\", mining must be able to emit it.\ncontract SweepProof is Test {\n    function test_sweptBoostBucketIsMinable() public {\n        vm.chainId(1);\n        vm.warp(1791847200 + 3 hours);\n        vm.etch(0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367, address(new Game()).code);\n        vm.store(0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367, 0, bytes32(uint256(2)));\n        HamsterVault vault = new HamsterVault(address(0xBEEF), address(new HamsterBoost()));\n        Tok t = new Tok();\n        t.mint(address(vault), 800_000_000 ether);\n        t.mint(address(1), 200_000_000 ether);\n        vm.prank(address(0xBEEF));\n        vault.bindToken(address(t));\n        vm.warp(vault.deployedAt() + 365 days);\n        vault.sweepDormantBoost();\n        uint256[] memory ws = new uint256[](1);\n        address miner = address(0xA11CE);\n        vault.claimMined(0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367, ws, miner);\n        (uint256 uncommitted,,,) = vault.bucketsLeft();\n        // Expected: the swept 50M is emitted by mining (uncommitted reaches 0 once all loss is mined).\n        // Actual: committed stops at 750M and bucketsLeft reports 50M uncommitted forever.\n        assertEq(vault.miningCommitted(), 800_000_000 ether, \"swept 50M never becomes minable\");\n        assertEq(uncommitted, 0);\n    }\n}","reproduction":"Deploy vault, bind token, have game v2 finalize window 0 with total eligible loss 100*18,750 IMD (far past the 90th halving). warp to deployedAt+365d, call sweepDormantBoost() (miningAllocation becomes 800M), then claimMined(v2,[0],miner). Expected: mining can commit the swept 50M (miningCommitted reaches 800M, bucketsLeft().miningUncommitted -> 0). Actual: miningCommitted == 750,000,000e18 and bucketsLeft().miningUncommitted == 50,000,000e18 forever; the proof test fails with '750000000000000000000000000 != 800000000000000000000000000'.","severity":"medium","title":"365-day dormant-boost sweep never makes the 50M minable; it is stranded in the vault forever"},{"description":"The random-sequence invariant suite covers mining on game v2, staking, vesting, unstaking, all three reward sources and forwarding. Game switches (48h timelock, range resolution, the 14-day 2M/day cap) are covered by deterministic unit tests and a fuzzed per-day cap test (test/EdgeCases.t.sol, test/Mining.t.sol) but not inside random call sequences.","line":1,"path":"test/VaultInvariants.t.sol","reproduction":"n/a (coverage note)","severity":"info","title":"Untested edge: invariant handler does not drive game switches"}],"hash":"5277909933f0d87922a40678e5f18112b1698a38c99846d99218a888d84902ed","nodeId":"4926e1aa-4b0b-40a9-a778-2e53bf2203f2","outcome":"completed","summary":"I found one defect: the 365-day sweep of the dormant boost never makes the 50M HAMSTER minable, so it stays locked in the vault for good. I reported it in `.imd-findings.json` instead of writing a test that treats it as correct. The full suite passes with the verifier's seed: 72 tests, 0 failures (the 52 existing tests plus 20 new ones), and I only added files under `test/`.\n\n**The defect (medium).** The brief says that if the boost isn't activated within 365 days, anyone can move the 50M into mining. `sweepDormantBoost()` only raises the `miningAllocation` variable from 750M to 800M, and nothing that pays out reads that variable. The emission curve stops at the fixed 750M, and the mining commit check compares against the same fixed 750M (`src/HamsterMining.sol:157`). So:\n- No amount of game loss can ever commit more than 750M.\n- `bucketsLeft()` shows 50M of mining left forever, so the site would advertise a bucket that can never pay.\n- There is no other way to get that HAMSTER out.\n\nThe finding includes a self-contained proof test. On the current code it fails with `750000000000000000000000000 != 800000000000000000000000000`. How the 50M should be emitted is a design choice the brief leaves open, so I didn't guess one.\n\n**`test/VaultInvariants.t.sol` (new):** a handler that makes random sequences of calls across three users: game losses, time jumps, mining claims, staking, unstaking, vesting withdrawals, all three reward sources, reward claims, forwarding and flushing. It ran 48 runs × 40 calls, with no calls rejected. After every sequence it checks that:\n- The vault's HAMSTER and IMD balances exactly equal what it owes, and forwarded IMD equals pending plus flushed.\n- Mining never exceeds 750M; no window pays more than its budget; and the window budgets add up exactly to the global halving curve.\n- Each user's grants plus stake match their positions, and no one withdraws more HAMSTER than they staked or were granted.\n- Earning weight equals only the stakes past their 24h warm-up, and an unstake request stops weight immediately.\n- Accrued plus paid IMD rewards never exceed what has been released, and released never exceeds what was deposited.\n\n**`test/EdgeCases.t.sol` (new):** 19 tests, mostly failure paths:\n- **TEAM powers:** every one rejects other callers; binding works once.\n- **Bind checks:** off-by-one-wei total supply and vault balance are rejected, as are IMD, an address with no code and the zero address.\n- **Game switch timelock:** boundaries at exactly 48h minus 1 second, rescheduling restarts the delay, and an old game can't be re-registered.\n- **Mining claims:** unknown games and unfinalized windows are rejected; a claim with zero loss uses up the slot; a game reporting dishonest loss figures is caught.\n- **Staking, vesting and rewards:** zero, too-large and out-of-range amounts and ids, duplicate ids in a batch, the exact second unstaked tokens release, and empty or oversized batches.\n- **Forwarding:** the jackpot and oracle balances stay separate, and forwarded IMD follows the hook through two game switches.\n- **Boost:** the activation deadline boundary, the mainnet-only and ERC-721 checks, and that calling the boost contract directly can't change the vault's boost state.\n- **Fuzz tests:** window budgets add up exactly across halvings when claimed in reverse order, and the new game's 2M-per-UTC-day cap holds on the switch day and the day after.\n\nThe random-sequence tests don't include game switches. Switches are covered by the deterministic tests and the fuzzed daily-cap test; I noted this gap in the findings file as info.","treeHash":"10bca31b9e2716685ed2c703475acf31a53abbd0","usage":{"cachedInputTokens":1385426,"inputTokens":32,"model":"claude-opus-5-5","outputTokens":34408,"runtime":"claude","turns":19,"wallClockMs":601968}},{"artifacts":[],"attempt":1,"bundleHash":"0399f11b18564e1369c06315986054f15c716e089f337ff5a4bd9a0ec1a0f250","device":"5d667e4b0751bcb5","findings":[],"hash":"73269e39124baf1c0d57b92465b0b9d3ec59fb75455ffc1e75b8f3d233b23932","nodeId":"2665a699-c66c-48a6-9a0a-18259e518514","outcome":"completed","summary":"The revision is complete. The medium finding reproduced and is fixed; the advisory finding is answered as disputed with no code change.\n\n**What was wrong.** A capped day's `raw` and `allowance` were summed on the curve in force when the day was cached, but each window of that day was later priced on the live curve. After a dormant-boost sweep inside a successor's 14-day cap period, the still-unclaimed windows of a day cached before it were priced on the extended curve against the stale base-curve denominator, so the day overpaid its 2M allowance and identical windows paid different amounts by claim time. The reviewer's proof failed on the starting tree exactly as reported.\n\n**What changed.**\n- `src/HamsterBoost.sol`: each cached capped day now stores the curve it was summed on in a new `capDayCurves` mapping, written next to `capDays`. Window pricing for a cached day uses that snapshot as a fresh memory struct, not an alias of the live state. The `CapDay` layout and getter are unchanged, so the proof still compiles.\n- `test/Mining.t.sol`: new regression test with a sweep landing inside the cap period, one day cached and half-claimed before the sweep and a second day cached after it. Both windows of the cached day pay 1,000,000 HAMSTER, the snapshots record base and the sweep point respectively, and emission equals committed. I confirmed that both this test and the proof fail with the old pricing line and pass with the fix.\n- README, docs/REVIEW.md, docs/VALIDATION.md and abi/HamsterBoost.json updated for the new getter, the behaviour, and the current sizes.\n- `.imd-responses.json` answers both findings.\n\n**Verification.** `forge build`, `forge fmt --check` and `forge test` with the verifier's fuzz seed all pass: 59 tests across 6 suites, 0 failures. The reviewer's proof passes on the fixed tree.\n\n**Advisory finding.** Halving on cap-adjusted effective loss rather than raw loss is the intended reading: halving on raw loss while paying only 2M per capped day would permanently forfeit emission, which the previous round's fix was asked to remove. It stays documented in the README as an item for the requester to confirm.","treeHash":"b138972bddbfb754e9320f5befafc96aa6c280cb","usage":{"cachedInputTokens":2441054,"inputTokens":802,"model":"claude-fable-5-1","outputTokens":34686,"runtime":"claude","turns":49,"wallClockMs":701372}},{"artifacts":[],"attempt":1,"bundleHash":"ad3dce1ee3755ac26b8215dc67004242eadb13bc7a30f3e274dd2e518e6787c7","device":"a7b8747ac077deb9","findings":[{"description":"capDays caches a capped day's `raw` (sum of window budgets) and `allowance` on the curve in force when the day is prepared. _rawBudget then prices each unprepared window of that day with the *current* curve: `raw_k = _curve(s, pos + total) - _curve(s, pos)` and returns `allowance * raw_k / d.raw`. A 365-day sweep (sweepDormantBoost) scales the curve beyond `sweptLoss` by (room + 50M) / room. sweptLoss is the effective (capped) position, but a capped day's windows sit at raw positions up to `d.pos + full day loss`, which is beyond it. So after the sweep, the day's unclaimed windows compute a larger raw_k against the stale base-curve denominator d.raw. The sum of the day's window budgets then exceeds its cached allowance, so the day pays more than the 2M new-game cap. A window claimed before the sweep is paid less than an identical window claimed after it, which breaks claim-order/claim-time independence. The day is cached by any claim, resolveGame or permissionless prepareCap; the windows stay unprepared until someone claims them. Suggested fix: price window shares on a capped day with the same curve that produced d.raw. Either store per-day base-curve shares, or recompute raw_k with the curve as it was before the sweep for positions inside a prepared day (for example by making sweptLoss at least every prepared day's raw end).","line":649,"path":"src/HamsterBoost.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\ncontract ProofToken {\n    uint256 public totalSupply;\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract ProofGame {\n    address public constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    uint64 public immutable genesis;\n    uint256 public nextFinalizeWindow;\n    uint256 public cumulativeLoss;\n    mapping(uint256 => mapping(address => uint256)) public lossOf;\n    mapping(uint256 => uint256) public totalEligibleLoss;\n    mapping(uint256 => uint256) public cumulativeLossBefore;\n    mapping(uint256 => uint256) public cumulativeLossAfter;\n    mapping(address => uint256) public balances;\n\n    constructor(uint64 genesis_) {\n        genesis = genesis_;\n    }\n\n    function windowStart(uint256 k) external view returns (uint64) {\n        return uint64(genesis + k * 1 hours);\n    }\n\n    function windowEnd(uint256 k) public view returns (uint64) {\n        return uint64(genesis + (k + 1) * 1 hours);\n    }\n\n    function setLoss(uint256 k, address who, uint256 amount) external {\n        totalEligibleLoss[k] = totalEligibleLoss[k] - lossOf[k][who] + amount;\n        lossOf[k][who] = amount;\n    }\n\n    function finalize(uint256 maxWindows) external {\n        for (uint256 i; i < maxWindows; ++i) {\n            uint256 k = nextFinalizeWindow;\n            if (windowEnd(k) > block.timestamp) break;\n            cumulativeLossBefore[k] = cumulativeLoss;\n            cumulativeLoss += totalEligibleLoss[k];\n            cumulativeLossAfter[k] = cumulativeLoss;\n            ++nextFinalizeWindow;\n        }\n    }\n\n    function withdraw() external {}\n    function depositJackpot(uint256) external {}\n    function fundOracleReserve(uint256) external {}\n}\n\n/// A 365-day dormant-boost sweep that happens after a capped new-game day has been cached\n/// (capDays stores raw/allowance on the base curve) but before all of that day's windows are\n/// prepared makes the remaining windows price their raw budget on the extended curve while the\n/// cached denominator `raw` stays on the base curve. The day then emits more than its 2M cap.\ncontract SweepAfterCachedCapDayProof is Test {\n    address constant TEAM = 0x6CBA4751742F6989fA224B2dC5d5d52fCef98a34;\n    address constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    address constant V2 = 0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367;\n    uint64 constant GENESIS = 1791847200;\n\n    function test_cappedDayNeverExceeds2MAcrossSweep() public {\n        vm.chainId(1);\n        vm.etch(IMD, address(new ProofToken()).code);\n        vm.etch(V2, address(new ProofGame(GENESIS)).code);\n        // Deploy so that the 365-day sweep falls inside a successor's 14-day cap period.\n        vm.warp(uint256(GENESIS) - 360 days);\n        HamsterBoost boost = new HamsterBoost();\n        HamsterVault vault = new HamsterVault(TEAM, address(boost));\n        ProofToken hamster = new ProofToken();\n        hamster.mint(address(vault), 800_000_000 ether);\n        hamster.mint(address(this), 200_000_000 ether);\n        vm.prank(TEAM);\n        vault.bindToken(address(hamster));\n\n        vm.warp(uint256(GENESIS) + 2 hours);\n        ProofGame next = new ProofGame(GENESIS);\n        vm.prank(TEAM);\n        vault.scheduleGame(address(next));\n        vm.warp(block.timestamp + 48 hours);\n        vault.executeGame();\n        uint256 day = block.timestamp / 1 days + 1;\n        uint256 first = (day * 1 days - GENESIS + 1 hours - 1) / 1 hours;\n        address alice = makeAddr(\"alice\");\n        address bob = makeAddr(\"bob\");\n        // 300 IMD of loss on one capped day: raw 6M, capped to 2M.\n        next.setLoss(first, alice, 150 ether);\n        next.setLoss(first + 1, bob, 150 ether);\n        vm.warp((day + 1) * 1 days + 1 hours);\n        ProofGame(V2).finalize(1000);\n        next.finalize(1000);\n        vault.resolveGame(address(next));\n        // Anyone caches the capped day (any claim on a later day does this too).\n        boost.prepareCap(address(vault), address(next), 16);\n        (,,, uint256 raw, uint256 allowance) = boost.capDays(address(vault), address(next), day);\n        assertEq(raw, 6_000_000 ether);\n        assertEq(allowance, 2_000_000 ether);\n\n        _catchUp(vault);\n        // Alice claims before the sweep and gets exactly half the capped day.\n        assertEq(vault.claimMined(address(next), _one(first), alice), 1_000_000 ether);\n\n        // Day 365: nobody activated the boost; anyone sweeps.\n        assertLt(block.timestamp, vault.deployedAt() + 365 days);\n        vm.warp(vault.deployedAt() + 365 days);\n        vault.sweepDormantBoost();\n        _catchUp(vault);\n\n        // Bob's window carries the same loss in the same capped day: it must not pay more than\n        // the 1M remaining in the day's 2M allowance.\n        uint256 paid = vault.claimMined(address(next), _one(first + 1), bob);\n        assertLe(paid, 1_000_000 ether, \"bob paid more than the day's remaining cap\");\n        assertLe(1_000_000 ether + paid, 2_000_000 ether, \"capped day emitted more than 2M\");\n    }\n\n    function _catchUp(HamsterVault vault) internal {\n        for (uint256 i; i < 64 && !vault.checkpoint(128); ++i) {}\n    }\n\n    function _one(uint256 x) internal pure returns (uint256[] memory xs) {\n        xs = new uint256[](1);\n        xs[0] = x;\n    }\n}","reproduction":"Deploy the vault so that its deployedAt + 365 days falls inside a successor's 14-day cap period (deployedAt = V2 genesis - 360 days). Switch to a successor at genesis + 50h. On the next UTC day, put 150 IMD loss for alice in its first window and 150 IMD for bob in the second (raw 6M, allowance 2M, consumed 100 IMD). After the day is finalized, resolve and call boost.prepareCap(vault, successor, 16). Alice claims: 1,000,000 HAMSTER (expected). Warp to deployedAt + 365 days and call sweepDormantBoost(). Bob then claims his identical window. Expected: at most 1,000,000 HAMSTER (day total <= 2M). Actual: 1,066,844.919786096256684492 HAMSTER, so the day pays 2,066,844.9 HAMSTER.","severity":"medium","title":"Dormant-boost sweep after a capped new-game day is cached lets that day pay more than 2M (and makes payout depend on claim timing)"},{"description":"The brief requires `balanceOf(vault) == 8e26` at bind. The implementation checks `< 800_000_000 ether`, so any balance >= 800M is accepted and the excess becomes an unrescuable donation. The README states this is deliberate: a stray transfer before bind should not be able to veto binding. It is a deliberate deviation from the brief, not a correctness defect. The requester should confirm it. Tests assert both exact 8e26 and 8e26 + 1 bind, and 8e26 - 1 is rejected.","line":56,"path":"src/VaultBase.sol","reproduction":"Mint 1e27 HAMSTER with 8e26 + 1 wei in the vault and call bindToken as TEAM. Brief: revert. Actual: binds; hamsterLiability stays 8e26 and 1 wei is stranded.","severity":"info","title":"bindToken accepts a vault balance above 800M although the brief says balanceOf(vault) == 8e26"},{"description":"With the deferral design, loss that a new game's 14-day cap leaves unconsumed (`dropped`) is subtracted from the global position: globalOrigin of the next game, post-cap windows and currentMiningState. Halvings therefore happen later than 'every 18,750 IMD of global cumulative loss' measured on raw loss. The design is documented in docs/REVIEW.md and still converges to the bucket, but it reads the brief's halving rule differently. The requester should confirm it.","line":610,"path":"src/HamsterBoost.sol","reproduction":"test/Mining.t.sol test_newGameCapCrossingHalvingKeepsCurveConsistentAndChainsToNextGame: a successor reports 20,100 IMD of loss (past the first 18,750 halving), yet currentMiningState reports globalLoss 200 IMD, halving 0, rate 20,000.","severity":"info","title":"Halving index follows the cap-adjusted effective loss, not the raw global cumulative loss"}],"hash":"823ee0cd9c1b8e066c7b26e6da5e19cf55cc3db1739bd148e462e96aaa29d5d3","nodeId":"4926e1aa-4b0b-40a9-a778-2e53bf2203f2","outcome":"completed","summary":"I updated my tests for the revised HamsterVault and found one real defect, which I've reported in `.imd-findings.json` rather than writing a test that passes over it. `forge test` passes with the required fuzz seed: 81 tests across 9 suites, no failures. The only failing test is the proof file in `test/scratch/`, and it is meant to fail.\n\n**Defect (medium): the 365-day sweep can push a capped day above 2M.** A new game's capped day stores its totals using the emission curve in force when it is first computed. After `sweepDormantBoost()` raises the curve toward 800M, windows of that day that nobody has claimed yet are priced on the new curve, but still divided by the old total.\n- **Reproduction:** two identical windows of 150 IMD loss on one capped day. Alice claims before the sweep and gets 1,000,000 HAMSTER. Bob claims after it and gets 1,066,844.9.\n- **Effect:** that day pays about 2.067M against its 2M cap. The amount also depends on whether you claim before or after the sweep.\n- **Trigger:** it only happens when the sweep falls inside a new game's 14-day cap period.\n\nThe proof test is in the findings file, along with a suggested fix.\n\n**Two info notes for you to confirm.** Both are deliberate design choices by the implementer, not bugs:\n- `bindToken` accepts a vault balance of 800M or more, where the brief says exactly 800M. The README says this stops a stray transfer from blocking the bind.\n- Loss that the new-game cap holds back is not counted toward halvings. So halvings come later than \"every 18,750 IMD of raw loss\".\n\n**What I changed in the tests:**\n- **Updated for the new code:** `rateAndHalving` now lives on HamsterBoost, so the test calls it there. Only the grantee can now call `withdrawVested`, and other callers are tested to fail. While a boost activation is pending, TEAM can now replace it, but only while it can still be executed before day 365. After execution it cannot be scheduled or executed again.\n- **Fixed a silent gap in my invariant handler:** its `withdrawVested` call had started failing every time, and those failures were being ignored. It now calls as the grantee (133 calls, none failing), and it checks that nobody else can withdraw.\n- **New tests in `test/EdgeCases.t.sol`:**\n  - `prepareCap` input limits, plus unknown and not-yet-resolved games.\n  - A fuzz test of a capped day at random points on the curve, across halvings. It checks the day never pays more than 2M, an over-cap day pays within 1e12 wei of 2M, and the curve only advances by what was paid.\n- **New invariant suite, `test/SwitchInvariants.t.sol`:** random losses, time jumps, up to two game switches, resolving games and claims on any game. It checks the 2M daily cap, the mining bucket, that claims match `claimable`, and HAMSTER custody.\n\nTwo limits on the new suite:\n- It never calls the sweep, so it cannot catch the defect above.\n- A quick check showed it does make claims and switch games, but I stopped that check before it finished.","treeHash":"f14c5ba271c4a63cd12df8dfeec5d5b034980353","usage":{"cachedInputTokens":3457041,"inputTokens":76,"model":"claude-opus-5-5","outputTokens":37311,"runtime":"claude","turns":40,"wallClockMs":921898}},{"artifacts":[],"attempt":1,"bundleHash":"2e9a454162aae898d2c3861aabffef0c22b13a70c3c0633cef4cb32ad76df474","device":"3b260b68e9ad6a37","findings":[],"hash":"88b19dd1c10a00b6f71ab142172bb3bd52000f82f5b86a1337d2ce15c25b7e67","nodeId":"2665a699-c66c-48a6-9a0a-18259e518514","outcome":"completed","summary":"The responses file is excluded by the harness's own `.git/info/exclude`, which is expected: it is read from disk by the job, not committed. The work is complete.\n\n## Summary\n\nAll five reopened findings are fixed, both reviewer proofs pass, and the suite is green: 60 tests pass under `forge build`, `forge test` with the verifier's fuzz seed, and `forge fmt --check`. The vault stays under EIP-170 (24,421 bytes, 155 margin) after moving new logic into the engine.\n\n**Fixed (required):**\n- **Bind griefing** (`src/VaultBase.sol`): `bindToken` now accepts a vault balance of at least 800M. Excess is a donation, consistent with the existing liability accounting.\n- **Swept 50M unminable** (`src/HamsterBoost.sol`): the sweep records the finalized effective global loss, and the curve beyond that point is scaled to converge to 800M. The base curve, halving points, and every pre-sweep window are unchanged. Commitments are capped at the live `miningAllocation`.\n- **Vested weight stripping** (`src/HamsterStaking.sol`): `withdrawVested` requires the grantee as caller.\n- **Cap forfeiture** (`src/HamsterBoost.sol`): capped days are evaluated sequentially. A day over 2M pays exactly the 2M curve segment, scales its windows, and advances the curve only by the loss actually consumed. Later days, uncapped windows, and the next game resume from that effective position, so nothing is stranded. Verified across a halving crossing and a second switch.\n- **Hook deposit liveness**: `depositRewards` and `depositDrip` attempt a bounded 16-event checkpoint and then anchor to the stored clock instead of reverting.\n\n**Advisory findings:** boost rescheduling, the mining view during an unresolved switch, and the quadratic cap gas (now cached via permissionless `prepareCap`, 24 capped windows cost 4.3M gas instead of 16.4M) are fixed. Boost preparation cost and the stalled-successor case are answered as disputed with reasoning, and the README documents the successor's permissionless-finalization requirement.\n\n**Also delivered:** README and docs rewritten for the new cap, sweep, bind, and deposit semantics; ABI files regenerated; `rateAndHalving` moved to the engine to fit the size limit. Full answers for all ten findings are in `.imd-responses.json`.","treeHash":"42da5f8fa0409ba3495e2d1350f0fadf527675f3","usage":{"cachedInputTokens":3356834,"inputTokens":738,"model":"claude-fable-5-1","outputTokens":75201,"runtime":"claude","turns":34,"wallClockMs":1479764}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a18a0c6087e1362f","findings":[{"citation":"resolved","description":"bindToken (src/VaultBase.sol:51-60) reverts InvalidAmount unless balanceOf(vault) == 800_000_000 ether exactly. The vault has no way to move HAMSTER before it is bound (every push path calls _bound()), so once any third party sends even 1 wei of HAMSTER to the vault address before TEAM's bind transaction lands, the equality can never be satisfied again and bindToken is permanently impossible. Launch 2 mints the token and pays 800M to this vault in the launch transaction; the pool exists from that moment, so a bot can buy a dust amount and transfer it to the vault ahead of TEAM's bind. Result: the 800M remainder (mining + boost buckets), staking and vesting are all dead; the IMD side keeps working but nothing can ever pay HAMSTER out. Cost to the attacker: one dust swap plus a transfer. The brief words the check as '== 8e26', but the same brief also makes unsolicited donations 'excess with no rescue path' (README, hamsterLiability), so treating excess as a donation here is consistent: check balanceOf(vault) >= 800M (and totalSupply == 1e27) and keep the liability base at 800M. The same equality on totalSupply bricks binding if the hook token burns any supply (anti-snipe tax burns) before bind; verify the launch-2 token cannot change totalSupply before bind. Economic-security guide: 'cheapest griefing vector that blocks other users'.","line":56,"path":"src/VaultBase.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\ncontract GriefToken {\n    uint256 public totalSupply;\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @notice A 1 wei HAMSTER donation to the vault before TEAM binds makes bindToken revert forever,\n/// locking the 800M remainder in an unbound vault with no withdrawal path.\ncontract BindGriefTest is Test {\n    address internal constant TEAM = address(0xBEEF);\n\n    function test_oneWeiDonationMustNotBrickBinding() public {\n        vm.chainId(1);\n        HamsterBoost boost = new HamsterBoost();\n        HamsterVault vault = new HamsterVault(TEAM, address(boost));\n        GriefToken token = new GriefToken();\n        address attacker = makeAddr(\"attacker\");\n        // Launch 2: 1B supply, exactly 800M remainder to the vault, 200M to the pool/others.\n        token.mint(address(vault), 800_000_000 ether);\n        token.mint(attacker, 200_000_000 ether);\n\n        // Attacker front-runs TEAM's bind with a 1 wei transfer to the vault.\n        vm.prank(attacker);\n        token.transfer(address(vault), 1);\n\n        // TEAM must still be able to bind: the excess is an unsolicited donation, not a reason\n        // to strand 800M HAMSTER forever. Currently reverts InvalidAmount.\n        vm.prank(TEAM);\n        vault.bindToken(address(token));\n        assertEq(vault.token(), address(token));\n    }\n}","reproduction":"State: HamsterVault deployed, launch-2 token minted with 800M to the vault, TEAM has not yet called bindToken. Attacker (any holder of >=1 wei HAMSTER) calls token.transfer(vault, 1). TEAM calls vault.bindToken(token). Expected: binding succeeds (excess 1 wei is a donation). Actual: revert InvalidAmount(); every later bindToken call reverts the same way, and no function can move HAMSTER out of the vault. Proof: test/scratch/BindGrief.t.sol fails with InvalidAmount() on the current code.","severity":"high","snippet":"                || IToken(token_).balanceOf(address(this)) != 800_000_000 ether || IToken(token_).decimals() != 18","title":"bindToken requires an exact 800M balance: a 1 wei HAMSTER donation before binding locks the vault forever"},{"citation":"resolved","description":"The brief says that if boost is not activated within 365 days 'anyone can move the 50M into mining once'. sweepDormantBoost only increments the view variable miningAllocation. Window budgets are F(after)-F(before) on emissionCurve (src/HamsterBoost.sol:370-376), whose integral saturates at MINING_ALLOCATION = 750M, and _prepareWindow still caps miningCommitted at the constant MINING_ALLOCATION (src/HamsterMining.sol:157). So after the sweep the sum of all window budgets over the entire future is still at most 750M; the 50M can never be committed, granted or claimed by anyone, and the contract has no owner, rescue or withdrawal path. README/docs/REVIEW.md acknowledge this ('the extra reserve therefore remains locked under this immutable curve'), i.e. the implementation knowingly turns 'move into mining' into 'lock forever'. Invariant guide: capacity constraint whose cap (750M in the curve) is not raised when the bucket (miningAllocation) is. Fix must be a scope decision by the author: e.g. make the curve's asymptote the live miningAllocation (scaling the remaining bands by 800/750 from the sweep point), or add a documented second term, so that bucketsLeft().miningUncommitted is actually emittable.","line":57,"path":"src/HamsterVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\ncontract SweepToken {\n    uint256 public totalSupply;\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract SweepGame {\n    address public constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    uint64 public constant genesis = 1791847200;\n    uint256 public nextFinalizeWindow;\n    uint256 public cumulativeLoss;\n    mapping(uint256 => mapping(address => uint256)) public lossOf;\n    mapping(uint256 => uint256) public totalEligibleLoss;\n    mapping(uint256 => uint256) public cumulativeLossBefore;\n    mapping(uint256 => uint256) public cumulativeLossAfter;\n    mapping(address => uint256) public balances;\n\n    function windowStart(uint256 k) external pure returns (uint64) {\n        return uint64(genesis + k * 1 hours);\n    }\n\n    function windowEnd(uint256 k) public pure returns (uint64) {\n        return uint64(genesis + (k + 1) * 1 hours);\n    }\n\n    function setLoss(uint256 k, address who, uint256 amount) external {\n        totalEligibleLoss[k] = totalEligibleLoss[k] - lossOf[k][who] + amount;\n        lossOf[k][who] = amount;\n    }\n\n    function finalize(uint256 maxWindows) external {\n        for (uint256 i; i < maxWindows; ++i) {\n            uint256 k = nextFinalizeWindow;\n            if (windowEnd(k) > block.timestamp) break;\n            cumulativeLossBefore[k] = cumulativeLoss;\n            cumulativeLoss += totalEligibleLoss[k];\n            cumulativeLossAfter[k] = cumulativeLoss;\n            ++nextFinalizeWindow;\n        }\n    }\n\n    function withdraw() external {}\n    function depositJackpot(uint256) external {}\n    function fundOracleReserve(uint256) external {}\n}\n\n/// @notice After sweepDormantBoost the 50M boost bucket is \"moved into mining\", but the emission\n/// curve still saturates at 750M, so the 50M can never be committed to any window: it is locked\n/// forever in an immutable contract with no rescue path.\ncontract SweepLockTest is Test {\n    address internal constant TEAM = address(0xBEEF);\n    address internal constant V2 = 0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367;\n    uint256 internal constant GENESIS = 1791847200;\n\n    function test_sweptBoostBucketMustBeMinable() public {\n        vm.chainId(1);\n        vm.warp(GENESIS + 2 hours);\n        SweepGame template = new SweepGame();\n        vm.etch(V2, address(template).code);\n        SweepGame game = SweepGame(V2);\n        HamsterBoost boost = new HamsterBoost();\n        HamsterVault vault = new HamsterVault(TEAM, address(boost));\n        SweepToken token = new SweepToken();\n        token.mint(address(vault), 800_000_000 ether);\n        token.mint(address(this), 200_000_000 ether);\n        vm.prank(TEAM);\n        vault.bindToken(address(token));\n\n        // Boost never activated: after 365 days anyone sweeps the 50M into mining.\n        vm.warp(GENESIS + 2 hours + 365 days);\n        vault.sweepDormantBoost();\n        assertEq(vault.miningAllocation(), 800_000_000 ether);\n\n        // Saturate the curve: 2,000,000 IMD of eligible loss in window 0 (h >= 90 halvings).\n        address alice = makeAddr(\"alice\");\n        game.setLoss(0, alice, 2_000_000 ether);\n        game.finalize(1);\n        uint256[] memory windows = new uint256[](1);\n        windows[0] = 0;\n        vault.claimMined(V2, windows, alice);\n\n        // Everything the curve can ever emit has now been committed. The swept 50M must be part of\n        // what mining can emit; currently it is stranded as an uncommittable \"reserve\".\n        (uint256 miningUncommitted,,,) = vault.bucketsLeft();\n        assertLt(miningUncommitted, 1 ether, \"swept 50M is not minable\");\n    }\n}","reproduction":"Deploy vault; bind token; warp to deployedAt + 365 days; call sweepDormantBoost() (miningAllocation becomes 800M, bucketsLeft reports miningUncommitted = 800M). Then give window 0 of v2 2,000,000 IMD of eligible loss (h >= 90, curve saturated) and claimMined(V2,[0],alice): miningCommitted becomes exactly 750M and bucketsLeft().miningUncommitted stays 50,000,000e18 while emissionCurve(x) can never exceed 750M, so no future window can ever carry a non-zero budget. Expected: the swept 50M is minable; actual: locked forever. Proof: test/scratch/SweepLock.t.sol fails with 'swept 50M is not minable: 5e25 >= 1e18'.","severity":"high","snippet":"        miningAllocation += BOOST_ALLOCATION;","title":"sweepDormantBoost moves the 50M boost bucket into a mining reserve the 750M emission curve can never commit"},{"citation":"resolved","description":"For the 14 days after a switch, windowBudget scales each window of a UTC day by 2M/sum (src/HamsterBoost.sol:434-445). The scaled budget is what _prepareWindow commits (src/HamsterMining.sol:149-160), but the global curve has already advanced by the raw amount (F(after)-F(before)), and nothing ever re-emits the withheld difference: miningCommitted can only reach 750M - sum(withheld). The brief describes the cap as a rate limit ('capped at 2M HAMSTER per UTC day, deterministic scaling') on a curve that 'converges to 750M'; the implementation turns it into a permanent burn of up to 48M - 2M = 46M per day at the initial rate, i.e. up to 644M of the 750M over the 14-day period, decided by player activity in the first two weeks of any successor game (including the planned Squeekchain migration). README states 'withheld amounts do not restart or shift the curve', so this is a documented choice, but it contradicts the stated 750M convergence and strands tokens with no rescue path. Who loses: every future miner; nobody gains. Suggested scope decision: carry the withheld amount forward (e.g. add it to a deferred pool emitted after the cap period), or compute the cap on the curve position so that the curve only advances by what is actually committed.","line":444,"path":"src/HamsterBoost.sol","reproduction":"test/scratch/CapStrand.t.sol (exploratory run, passes by construction and prints the numbers): bind token; scheduleGame(v3 with same genesis), warp 48h, executeGame; first full UTC day of v3 has 24 windows with 100 IMD eligible loss each (2,400 IMD, h=0, rate 20,000/IMD). After the day ends and both games finalize, resolveGame(v3) and claimMined(v3, 24 windows, alice). Observed: granted 1,999,999,999,999,999,999,999,992 (2M, the cap); emissionCurve(globalLoss) = 48,000,000e18 consumed; miningCommitted = 2M; bucketsLeft().miningUncommitted = 748,000,000e18 but the maximum future emission 750M - F(globalLoss) = 702,000,000e18. Expected: the 46M difference is deferred and emitted later; actual: 46M is unreachable forever (and 14 such days strand 644M).","severity":"medium","snippet":"        if (sum > NEW_GAME_DAILY_CAP) budget = M.mulDiv(budget, NEW_GAME_DAILY_CAP, sum);","title":"New-game daily cap forfeits the withheld emission instead of deferring it: a busy switch permanently strands most of the 750M"},{"citation":"resolved","description":"depositRewards and depositDrip (src/HamsterStaking.sol:329-343) call _readyGlobal, which reverts when more than MAX_CHECKPOINT (128) activations or stream ends matured since the last checkpoint. Every stake and every claimMined/claimBoost grant creates an activation 24h later at its own second; every depositRewards creates a stream end 7 active days later. The brief says the launch-2 hook 'sends IMD here' (0.5% of swaps and 80% of the anti-snipe tax); if that is done inline from afterSwap, then after a burst of >128 distinct-second claims or stakes followed by 24h without any vault transaction, the first swap reverts until someone calls checkpoint(128) enough times. This is a liveness/economic coupling between the vault and the pool, not a loss of funds.","line":206,"path":"src/HamsterStaking.sol","reproduction":"State: bound vault, alice stakes 1 wei at 130 distinct timestamps (130 activations at t+24h each). Warp 25h with no vault calls. Hook calls vault.depositRewards(1 ether). Expected: deposit accepted. Actual: revert CheckpointRequired() (first _checkpoint processes 128 events and wallTime < block.timestamp). Mitigation: have the hook call checkpoint or tolerate the revert (try/catch and retry), or let deposits accept a non-current global state since a stream start is anchored at the reward clock which cannot move past the pending events anyway.","severity":"low","snippet":"        if (!_checkpoint(MAX_CHECKPOINT)) revert CheckpointRequired();","title":"Hook deposits revert CheckpointRequired after more than 128 unattended events, so a hook that calls depositRewards/depositDrip synchronously can have its swaps fail"},{"citation":"resolved","description":"prepare processes at most 8 registrations per call, in two passes, over registeredTokens (src/HamsterBoost.sol:227), which never shrinks (voided tokens still occupy a slot). Days are strictly sequential (s.nextDay). With N registered tokens each UTC day needs ceil(N/8)*2 calls, each paying up to 25 external lossOf calls per registration in pass 2. For N = 10,000 that is 2,500 calls per day forever; nobody is obliged to pay, and a day cannot be skipped, so claimBoost for later days stays unreachable until the backlog is worked through. Any holder can also inflate N cheaply by registering tokens that will never be eligible. Not a loss of funds; the 100k/day budget is what pays for keeping it alive.","line":256,"path":"src/HamsterBoost.sol","reproduction":"Activate boost; register 10,000 tokens (any owners). On the next day, prepareBoostDay(8) must be called 1,250 times to finish pass 1 and 1,250 more for pass 2 before days_[day].complete; claimBoost(day, id) reverts NotReady until then, and day+1 cannot start before day completes.","severity":"low","snippet":"        if (maxRegistrations == 0 || maxRegistrations > 8) revert TooMany();","title":"Boost day preparation enumerates every NFT ever registered, so a large collection makes each day cost hundreds of permissionless calls and boost falls behind"}],"hash":"8f6e2fb485ea2d2bdca732e780a83e733062697e5b2b410412e086190291f57d","nodeId":"deb1119c-947f-4dd9-8cea-9a596cad3e18","outcome":"completed","summary":"Review complete. The working tree holds only the findings file (plus the pre-existing pinned `.imd/` inputs); the scratch tests were deleted after their output was embedded in the report.\n\n**Findings written to `.imd-findings.json`** (5 findings, 38 coverage rows, all 33 listed entry points answered):\n\n1. **High, with proof:** `bindToken` demands an exact 800M balance. Anyone who sends 1 wei of HAMSTER to the vault before TEAM binds makes binding revert forever, and no path can move HAMSTER out of an unbound vault. The proof test fails with `InvalidAmount()` on the current code. The brief words the check as equality, so I flagged this as a spec-level conflict with a suggested `>=` fix that keeps donations as excess.\n\n2. **High, with proof:** `sweepDormantBoost` adds 50M to `miningAllocation`, but the emission curve still saturates at 750M and `_prepareWindow` still caps at the 750M constant. After a sweep the 50M can never be committed to any window. README documents this as intended, but the brief asks for the 50M to be moved into mining, not locked.\n\n3. **Medium:** the 14-day new-game cap withholds emission without deferring it. A scratch run showed one day of 2,400 IMD loss consuming 48M of the curve while only 2M is committed, leaving 46M permanently unmintable, up to 644M across the cap period of a busy successor game.\n\n4. **Low:** `depositRewards` and `depositDrip` revert `CheckpointRequired` when more than 128 events matured unattended, so a hook that deposits inline from swaps can have swaps fail until someone checkpoints.\n\n5. **Low:** boost day preparation enumerates every token ever registered, 8 per call, in two sequential passes, so a large collection makes each day cost thousands of permissionless calls and days cannot be skipped.\n\n**Coverage:** staking (warm-up, unstake, vesting as weight, zero-weight pauses, streams, drip), mining (retroactive claims, halving, order independence, bucket conservation), game switch and range resolution, forwarding and flush, boost registration, caps and sweep all traced and marked `holds` except where a finding applies. Static-analysis leads in my area (mulDiv XOR, weak-prng, the uint64 cast) were checked and are false positives. The existing 52-test suite passes.","treeHash":null,"usage":{"cachedInputTokens":2312113,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":58869,"runtime":"claude","turns":39,"wallClockMs":869908}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"98b4506bef931d13","findings":[{"citation":"resolved","description":"Residual of the previous round's medium finding b6405338, which is fixed for the case it reported: a day cached before the sweep keeps its base curve, the day's shares sum to at most its allowance, and a window pays the same whether claimed before or after the sweep (the attached proof Proof_b64053382718 passes on this tree). What remains: the curve a capped day is priced on is the live State at the moment _advanceCap caches it, and both prepareCap and sweep are permissionless. For a day whose windows are all finalized when the sweep becomes callable, sweep() sets sweptLoss to the effective position after walking that day (its capped x, 100 IMD in the reproduction), so if the day is cached after the sweep its raw sum is taken on the extended curve for every raw position beyond sweptLoss (the later windows of the day, scaled by (room + 50M) / room) while its allowance stays 2M. The day then pays the same 2M in total but splits it differently between its windows than the same finalized history cached one block earlier: early windows lose, late windows gain. Day total, curve/commit accounting and the 2M cap still hold; only the intra-day distribution depends on the mempool order of two unrewarded permissionless transactions, which the brief calls 'deterministic scaling' and the prepareCap comment calls 'a function of finalized history only'. The README documents that 'a capped day cached after the sweep is summed and priced on the extended curve', so this is a documented rule, but a late-window claimant can select it by sending sweepDormantBoost() before anyone calls prepareCap, and an early-window claimant can pre-empt it by calling prepareCap first. Preconditions: boost never activated, the sweep not yet called while a successor's 14-day cap period contains a capped day (any switch after deployedAt + 351 days until someone sweeps), and that day finalized but not yet cached when the sweep is sent. Magnitude is bounded by the extension factor on the part of the day beyond sweptLoss (about 2.1% of the day in the reproduction, 21,331 HAMSTER). Minimal fix: choose a capped day's pricing curve from history rather than from caching time, e.g. record CapCurve.swept = s.swept && d.pos >= s.sweptLoss (a day whose first capped window starts at or before the sweep point is summed on the base curve, one that starts after it on the extended curve), so prepareCap before or after the sweep yields the same shares; or have sweep() cache every finalized capped day of the current game before it takes sweptLoss.","line":553,"path":"src/HamsterBoost.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\ncontract SCToken {\n    uint256 public totalSupply;\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract SCGame {\n    address public constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    uint64 public immutable genesis;\n    uint256 public nextFinalizeWindow;\n    uint256 public cumulativeLoss;\n    mapping(uint256 => mapping(address => uint256)) public lossOf;\n    mapping(uint256 => uint256) public totalEligibleLoss;\n    mapping(uint256 => uint256) public cumulativeLossBefore;\n    mapping(uint256 => uint256) public cumulativeLossAfter;\n    mapping(address => uint256) public balances;\n\n    constructor(uint64 genesis_) {\n        genesis = genesis_;\n    }\n\n    function windowStart(uint256 k) external view returns (uint64) {\n        return uint64(genesis + k * 1 hours);\n    }\n\n    function windowEnd(uint256 k) public view returns (uint64) {\n        return uint64(genesis + (k + 1) * 1 hours);\n    }\n\n    function setLoss(uint256 k, address who, uint256 amount) external {\n        totalEligibleLoss[k] = totalEligibleLoss[k] - lossOf[k][who] + amount;\n        lossOf[k][who] = amount;\n    }\n\n    function finalize(uint256 maxWindows) external {\n        for (uint256 i; i < maxWindows; ++i) {\n            uint256 k = nextFinalizeWindow;\n            if (windowEnd(k) > block.timestamp) break;\n            cumulativeLossBefore[k] = cumulativeLoss;\n            cumulativeLoss += totalEligibleLoss[k];\n            cumulativeLossAfter[k] = cumulativeLoss;\n            ++nextFinalizeWindow;\n        }\n    }\n\n    function withdraw() external {}\n    function depositJackpot(uint256) external {}\n    function fundOracleReserve(uint256) external {}\n}\n\n/// The same finalized capped day pays its two windows differently depending on whether the\n/// permissionless prepareCap ran before or after the permissionless dormant-boost sweep.\ncontract SweepCapOrderTest is Test {\n    address internal constant TEAM = address(0xBEEF);\n    address internal constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    address internal constant V2 = 0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367;\n    uint64 internal constant GENESIS = 1791847200;\n\n    HamsterVault vault;\n    HamsterBoost boost;\n    SCToken hamster;\n    SCGame v2;\n    SCGame next;\n    address alice = address(0xA11CE);\n    address bob = address(0xB0B);\n    uint256 day;\n    uint256 first;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.etch(IMD, address(new SCToken()).code);\n        vm.etch(V2, address(new SCGame(GENESIS)).code);\n        v2 = SCGame(V2);\n        vm.warp(uint256(GENESIS) - 360 days);\n        boost = new HamsterBoost();\n        vault = new HamsterVault(TEAM, address(boost));\n        vm.warp(uint256(GENESIS) + 2 hours);\n        hamster = new SCToken();\n        hamster.mint(address(vault), 800_000_000 ether);\n        hamster.mint(alice, 200_000_000 ether);\n        vm.prank(TEAM);\n        vault.bindToken(address(hamster));\n        next = new SCGame(GENESIS);\n\n        vm.prank(TEAM);\n        vault.scheduleGame(address(next));\n        vm.warp(uint256(GENESIS) + 50 hours);\n        vault.executeGame();\n        day = block.timestamp / 1 days + 1;\n        first = (day * 1 days - GENESIS + 1 hours - 1) / 1 hours;\n        next.setLoss(first, alice, 150 ether);\n        next.setLoss(first + 1, bob, 150 ether);\n        vm.warp((day + 1) * 1 days + 2 hours);\n        v2.finalize(1000);\n        next.finalize(1000);\n        vault.resolveGame(address(next));\n        // Day `day` is finalized and resolvable but not yet cached; the sweep deadline arrives.\n        vm.warp(vault.deployedAt() + 365 days);\n    }\n\n    function _claimBoth() internal returns (uint256 paidAlice, uint256 paidBob) {\n        uint256[] memory w = new uint256[](1);\n        w[0] = first;\n        paidAlice = vault.claimMined(address(next), w, alice);\n        w[0] = first + 1;\n        paidBob = vault.claimMined(address(next), w, bob);\n    }\n\n    function test_capDayDependsOnSweepOrdering() public {\n        uint256 snap = vm.snapshotState();\n\n        // Order A: the day is cached, then the sweep lands.\n        boost.prepareCap(address(vault), address(next), 16);\n        vault.sweepDormantBoost();\n        (uint256 aliceA, uint256 bobA) = _claimBoth();\n        assertEq(aliceA, 1_000_000 ether, \"order A alice\");\n        assertEq(bobA, 1_000_000 ether, \"order A bob\");\n\n        vm.revertToState(snap);\n\n        // Order B: the sweep lands first, then the same finalized day is cached.\n        vault.sweepDormantBoost();\n        boost.prepareCap(address(vault), address(next), 16);\n        (,,, uint256 raw, uint256 allowance) = boost.capDays(address(vault), address(next), day);\n        emit log_named_uint(\"order B raw\", raw);\n        emit log_named_uint(\"order B allowance\", allowance);\n        (uint256 aliceB, uint256 bobB) = _claimBoth();\n        emit log_named_uint(\"order B alice\", aliceB);\n        emit log_named_uint(\"order B bob\", bobB);\n        assertLe(aliceB + bobB, allowance, \"day total within allowance\");\n        // Expected: the same finalized history pays the same shares whichever permissionless\n        // call ran first.\n        assertEq(aliceB, aliceA, \"alice share depends on sweep ordering\");\n        assertEq(bobB, bobA, \"bob share depends on sweep ordering\");\n    }\n}","reproduction":"test/scratch/SweepCapOrder.t.sol (fails on this tree). State: chainId 1, HamsterBoost + HamsterVault(TEAM, boost) deployed at GENESIS - 360 days (deployedAt + 365 days = GENESIS + 5 days); token bound; successor SCGame(GENESIS) scheduled at GENESIS + 2h and executed at GENESIS + 50h; on the next UTC day the successor's first window carries 150 IMD for alice and the second 150 IMD for bob (raw 6M, allowance 2M, 100 IMD consumed); both games finalized after that day and resolveGame(successor) called, the day NOT cached; warp to deployedAt + 365 days. Order A: boost.prepareCap(vault, successor, 16), then vault.sweepDormantBoost(), then claimMined for each window: alice 1,000,000e18, bob 1,000,000e18. Order B (vm.revertToState to the same state): vault.sweepDormantBoost(), then boost.prepareCap(vault, successor, 16), then the same claims: capDays raw = 6,267,379.679e18, allowance = 2,000,000e18, alice receives 978,668.941979522184300341e18 and bob 1,021,331.058020477815699658e18. Expected: the same finalized history pays the same shares whichever permissionless call ran first (alice 1,000,000e18, bob 1,000,000e18). Actual: assertion 'alice share depends on sweep ordering: 978668941979522184300341 != 1000000000000000000000000'.","severity":"low","snippet":"capDayCurves[vault][game][day] = CapCurve(s.swept, s.sweptLoss);","title":"A capped day finalized before the dormant-boost sweep is priced on whichever curve is live when prepareCap first caches it, so two permissionless calls decide the split of its 2M between its windows"}],"hash":"93b68141e7eb6baefa7d366c384fb79a5e08157c7a6961ab2808066947b75ec9","nodeId":"c6642f56-f20b-4acf-a70b-97ec790c8d0c","outcome":"completed","summary":"The review is complete and `.imd-findings.json` is written with one finding and a 37-row coverage record. No repository files were changed.\n\n**Prior findings settled**\n\n- **Medium b6405338 (sweep re-prices a cached capped day): fixed.** The attached proof now passes on this tree. Each cached day records the curve it was summed on in `capDayCurves`, and `_rawBudget` prices its windows on that snapshot. A day cached before the sweep keeps its base curve, its windows sum to at most the 2M allowance, and a window pays the same before or after the sweep. The full suite passes from a clean offline build.\n\n| Suite | Result |\n|---|---|\n| 11 suites, 85 tests | all pass |\n| Proof_b64053382718 (prior proof) | passes |\n\n- **Info efa013de (halving on effective loss): dropped.** The author's answer holds. Halving on raw loss while paying only 2M per capped day would forfeit the gap, which is what the deferral removed. The README documents the reading for the requester.\n\n**One new low finding, a residual of the same root cause**\n\nThe curve a capped day is priced on is whichever state is live when `prepareCap` first caches it, and both `prepareCap` and `sweep` are permissionless. For a day that is finalized but not yet cached when the sweep lands, the two call orders split the same 2M differently between the day's windows. In the reproduction, alice's window pays 978,669 instead of 1,000,000 and bob's pays 1,021,331. Day total, cap and commit accounting still hold, so this is a determinism break of about 2% of a day under narrow preconditions, not a loss. The README documents the rule, but a late-window claimant can select it by sending the sweep before anyone caches the day. The proof is in the file, and I suggested choosing the day's curve from its position relative to the sweep point rather than from caching time.\n\n**Coverage.** All 34 entry points are answered, 33 holds and one finding, plus rows for the two settled findings and the manifest. The specialists' older findings about the bind equality, the stranded 50M, cap forfeiture, third-party vested withdrawal, hook deposit reverts, the view during an unresolved switch and boost rescheduling are all addressed in the current code.","treeHash":null,"usage":{"cachedInputTokens":1829176,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":29950,"runtime":"claude","turns":28,"wallClockMs":557485}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6b16b4979d227241","findings":[],"hash":"9e7dd401d2539e3ebef009a7538cba5bde4c2e8067b33e92869fb19b121e64a7","nodeId":"2665a699-c66c-48a6-9a0a-18259e518514","outcome":"failed","summary":"the local build failed, so this was not submitted:\nforge build exited with 1:\ntring memory err) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\nNote: Candidate:\n  --> lib/forge-std/src/Test.sol:62:5:\n   |\n62 |     function assertEq(address left, address right) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\nNote: Candidate:\n  --> lib/forge-std/src/Test.sol:66:5:\n   |\n66 |     function assertEq(address left, address right, string memory err) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\nNote: Candidate:\n  --> lib/forge-std/src/Test.sol:70:5:\n   |\n70 |     function assertEq(bytes32 left, bytes32 right) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\nNote: Candidate:\n  --> lib/forge-std/src/Test.sol:74:5:\n   |\n74 |     function assertEq(bytes32 left, bytes32 right, string memory err) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\nNote: Candidate:\n  --> lib/forge-std/src/Test.sol:78:5:\n   |\n78 |     function assertEq(string memory left, string memory right) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\nNote: Candidate:\n  --> lib/forge-std/src/Test.sol:82:5:\n   |\n82 |     function assertEq(string memory left, string memory right, string memory err) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\nNote: Candidate:\n  --> lib/forge-std/src/Test.sol:86:5:\n   |\n86 |     function assertEq(bytes memory left, bytes memory right) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\nNote: Candidate:\n  --> lib/forge-std/src/Test.sol:90:5:\n   |\n90 |     function assertEq(bytes memory left, bytes memory right, string memory err) internal pure virtual {\n   |     ^ (Relevant source part starts here and spans across multiple lines).","treeHash":null,"usage":{"cachedInputTokens":2047584,"inputTokens":676,"model":"claude-fable-5-1","outputTokens":134921,"runtime":"claude","turns":61,"wallClockMs":1621686}},{"artifacts":[],"attempt":2,"bundleHash":"24f74591870d85314291909dd4e03fc9ee49e54d1977ccb9446d2c6e053e0b22","device":"0fde2559e5db10f2","findings":[],"hash":"a2384c73a15534f57d4ee05aabf0e5f648cc2ee6fae23929cfb5ad8e8fb2759f","nodeId":"2665a699-c66c-48a6-9a0a-18259e518514","outcome":"completed","summary":"Implemented the vault, immutable companion, ABI exports, tests, and deployment documentation.\n\nVerified: `forge build`, all **52 tests** with the requested seed, and `forge fmt --check` pass.\n\nThe README documents the sweep interpretation: mining allocation becomes 800M, while the fixed emissions curve remains capped at 750M. Mainnet deployment remains with IdentityMD’s launch service.","treeHash":"45cbae5878fc16714d8b126574f46733bb7aef7e","usage":{"cachedInputTokens":7949568,"inputTokens":184323,"model":"gpt-6-astra","outputTokens":92860,"runtime":"codex","turns":10,"wallClockMs":3062503}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"bd7adba3a8045853","findings":[{"citation":"resolved","description":"HamsterBoost._advanceCap caches a capped day's raw sum and allowance with the curve in force when the day is prepared (prepareCap is permissionless and is also triggered by claimMined, resolveGame and boost-day preparation). _rawBudget then prices every still-unprepared window of that day with the current State s: raw_k = _curve(s, pos+total) - _curve(s, pos), budget_k = allowance * raw_k / d.raw. sweep() sets sweptLoss to the effective (cap-adjusted) global position, but a capped day's windows sit at raw positions d.pos .. d.pos + full day loss, which extends beyond the effective position, so after the sweep the remaining windows of a day cached before it are evaluated on the extended curve (scaled by (room + 50M) / room) while d.raw still holds base-curve values. The sum of the day's window budgets then exceeds the cached allowance, the 2M new-game cap is exceeded and the curve/commit accounting (emissionCurve(globalLoss) == miningCommitted, which test_newGameDailyCapScalesEveryWindowAndDefersTheRest relies on) no longer holds. Two identical windows on the same day pay different amounts depending on whether they were claimed before or after the sweep, which breaks claim-order/claim-time independence; the same day cached after the sweep instead of before yields different per-window shares, so prepareCap's result is not a pure function of finalized history as its comment claims. Preconditions: a game switch between deployedAt + 351 days and deployedAt + 365 days (so the 365-day sweep lands inside the successor's 14-day cap period) with a capped day cached and not fully claimed at the sweep; the sweep is permissionless at that moment. Introduced by the fix for the sweep and cap-deferral findings of the previous round. Minimal fix: price a cached capped day with the same curve state that produced d.raw (store swept/sweptLoss in CapDay, or set sweptLoss at sweep to at least the raw end position d.pos + day loss of every cached capped day, or store per-window raw budgets when the day is cached).","line":651,"path":"src/HamsterBoost.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\ncontract SCToken {\n    uint256 public totalSupply;\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract SCGame {\n    address public constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    uint64 public immutable genesis;\n    uint256 public nextFinalizeWindow;\n    uint256 public cumulativeLoss;\n    mapping(uint256 => mapping(address => uint256)) public lossOf;\n    mapping(uint256 => uint256) public totalEligibleLoss;\n    mapping(uint256 => uint256) public cumulativeLossBefore;\n    mapping(uint256 => uint256) public cumulativeLossAfter;\n    mapping(address => uint256) public balances;\n\n    constructor(uint64 genesis_) {\n        genesis = genesis_;\n    }\n\n    function windowStart(uint256 k) external view returns (uint64) {\n        return uint64(genesis + k * 1 hours);\n    }\n\n    function windowEnd(uint256 k) public view returns (uint64) {\n        return uint64(genesis + (k + 1) * 1 hours);\n    }\n\n    function setLoss(uint256 k, address who, uint256 amount) external {\n        totalEligibleLoss[k] = totalEligibleLoss[k] - lossOf[k][who] + amount;\n        lossOf[k][who] = amount;\n    }\n\n    function finalize(uint256 maxWindows) external {\n        for (uint256 i; i < maxWindows; ++i) {\n            uint256 k = nextFinalizeWindow;\n            if (windowEnd(k) > block.timestamp) break;\n            cumulativeLossBefore[k] = cumulativeLoss;\n            cumulativeLoss += totalEligibleLoss[k];\n            cumulativeLossAfter[k] = cumulativeLoss;\n            ++nextFinalizeWindow;\n        }\n    }\n\n    function withdraw() external {}\n    function depositJackpot(uint256) external {}\n    function fundOracleReserve(uint256) external {}\n}\n\n/// A dormant-boost sweep that lands inside a successor's 14-day cap period, after a capped day\n/// has been cached, lets that day pay more than its 2M allowance and makes a window's payout\n/// depend on whether it was claimed before or after the sweep.\ncontract SweepCapTest is Test {\n    address internal constant TEAM = address(0xBEEF);\n    address internal constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    address internal constant V2 = 0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367;\n    uint64 internal constant GENESIS = 1791847200;\n\n    HamsterVault vault;\n    HamsterBoost boost;\n    SCToken hamster;\n    SCGame v2;\n    SCGame next;\n    address alice = address(0xA11CE);\n    address bob = address(0xB0B);\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.etch(IMD, address(new SCToken()).code);\n        vm.etch(V2, address(new SCGame(GENESIS)).code);\n        v2 = SCGame(V2);\n        // Deploy 360 days before v2 genesis so that deployedAt + 365 days falls 5 days after\n        // genesis, inside the successor's cap period.\n        vm.warp(uint256(GENESIS) - 360 days);\n        boost = new HamsterBoost();\n        vault = new HamsterVault(TEAM, address(boost));\n        vm.warp(uint256(GENESIS) + 2 hours);\n        hamster = new SCToken();\n        hamster.mint(address(vault), 800_000_000 ether);\n        hamster.mint(alice, 200_000_000 ether);\n        vm.prank(TEAM);\n        vault.bindToken(address(hamster));\n        next = new SCGame(GENESIS);\n    }\n\n    function test_sweepAfterCachedCappedDayOverpaysTheDay() public {\n        vm.prank(TEAM);\n        vault.scheduleGame(address(next));\n        vm.warp(uint256(GENESIS) + 50 hours);\n        vault.executeGame();\n        uint256 switchedAt = block.timestamp;\n        uint256 day = switchedAt / 1 days + 1;\n        uint256 first = (day * 1 days - GENESIS + 1 hours - 1) / 1 hours;\n        next.setLoss(first, alice, 150 ether);\n        next.setLoss(first + 1, bob, 150 ether);\n        vm.warp((day + 1) * 1 days + 2 hours);\n        v2.finalize(1000);\n        next.finalize(1000);\n        vault.resolveGame(address(next));\n        boost.prepareCap(address(vault), address(next), 16);\n        (, uint256 pos, uint256 lossStart, uint256 raw, uint256 allowance) = boost.capDays(address(vault), address(next), day);\n        assertEq(raw, 6_000_000 ether, \"raw day budget\");\n        assertEq(allowance, 2_000_000 ether, \"allowance\");\n\n        uint256[] memory w = new uint256[](1);\n        w[0] = first;\n        uint256 paidAlice = vault.claimMined(address(next), w, alice);\n        assertEq(paidAlice, 1_000_000 ether, \"alice half of the allowance\");\n        (uint256 bobBefore,) = vault.windowBudget(address(next), first + 1);\n        assertEq(bobBefore, 1_000_000 ether, \"bob's window before the sweep\");\n\n        // The 365-day sweep lands inside the cap period, after the day was cached.\n        vm.warp(vault.deployedAt() + 365 days);\n        vault.sweepDormantBoost();\n\n        (uint256 bobAfter,) = vault.windowBudget(address(next), first + 1);\n        w[0] = first + 1;\n        uint256 paidBob = vault.claimMined(address(next), w, bob);\n        assertEq(paidBob, bobAfter);\n        // Expected: the day pays at most its 2M allowance, and an identical window pays the same\n        // whether claimed before or after the sweep.\n        assertLe(paidAlice + paidBob, allowance, \"capped day pays more than its allowance\");\n        assertEq(paidBob, paidAlice, \"identical windows differ by claim time\");\n    }\n}","reproduction":"test/scratch/SweepCap.t.sol (fails on this tree). State: chainId 1, HamsterBoost + HamsterVault(TEAM, boost) deployed at GENESIS - 360 days so deployedAt + 365 days = GENESIS + 5 days; token bound; successor SCGame(GENESIS) scheduled at GENESIS + 2h and executed at GENESIS + 50h. On the next UTC day the successor's first window of that day carries 150 IMD for alice and the second 150 IMD for bob (raw 6M, allowance 2M, 100 IMD consumed). After the day is finalized: resolveGame(successor); boost.prepareCap(vault, successor, 16); claimMined(successor, [first], alice) pays 1,000,000e18 and windowBudget(successor, first+1) reads 1,000,000e18. Then warp to deployedAt + 365 days, sweepDormantBoost(), and claimMined(successor, [first+1], bob). Expected: bob receives 1,000,000e18 (day total <= 2M allowance; identical windows pay the same regardless of claim time). Actual: bob receives 1,066,844.919786096256684492e18, so the day pays 2,066,844.92e18 > 2,000,000e18 (assertion 'capped day pays more than its allowance: 2066844919786096256684492 > 2000000000000000000000000').","severity":"medium","snippet":"            uint256 raw = _curve(s, pos + total) - _curve(s, pos);\n            return (d.raw == 0 ? 0 : M.mulDiv(d.allowance, raw, d.raw), total);","title":"A dormant-boost sweep after a capped day is cached re-prices the day's unclaimed windows on the extended curve against the stale base-curve denominator, so the day pays more than its 2M allowance and "},{"citation":"resolved","description":"The deferral design that fixed the previous round's cap-forfeit finding leaves loss that a successor's 14-day cap did not consume out of the curve position (`dropped`), so halvings occur later than 'every 18,750 IMD of global cumulative loss' measured on raw loss. The curve still converges to the allocation and nothing is forfeited; this is a documented reading of the brief (README 'For windows starting in the first 14 days after a switch'), not a correctness defect, and is listed so the requester can confirm it. Not blocking.","line":617,"path":"src/HamsterBoost.sol","reproduction":"test/Mining.t.sol test_newGameCapCrossingHalvingKeepsCurveConsistentAndChainsToNextGame (passes on this tree): a successor's capped window carries 20,000 IMD of eligible loss, past the first 18,750 IMD halving point on raw loss, yet currentMiningState() returns globalLoss 200e18, halving 0 and rate 20,000e18 because only 100 IMD per capped day was consumed and 19,900 IMD were recorded as dropped. Expected under a raw-loss reading: halving 1, rate 10,000e18. Actual: halving 0, rate 20,000e18.","severity":"info","snippet":"        globalLoss = _effectiveLoss(vault, game, g, s, end);\n        halving = globalLoss / HALF_LOSS;","title":"Halving index and rate follow the cap-adjusted effective loss, not raw global cumulative loss (interpretation to confirm with the requester)"}],"hash":"a4f8f9b6ba7216098b57e81799ef0d2b9913a76d70b9c6a8f4ca1d28b24cb367","nodeId":"c6642f56-f20b-4acf-a70b-97ec790c8d0c","outcome":"completed","summary":"The review is complete and written to `.imd-findings.json`. Nothing in the tree outside the ignored scratch and findings paths was changed.\n\n**Settled from the previous round.** Every reproducible finding is fixed and I confirmed each against the revised code:\n\n- **bindToken 1-wei brick (critical):** fixed. The prior proof now passes, and the check is `balanceOf >= 800M` with supply and decimals unchanged.\n- **Swept 50M unmineable (high):** fixed. The prior proof passes. The curve extends past the sweep point to converge on 800M and the commit cap reads the live allocation.\n- **withdrawVested open to anyone (medium):** fixed. A third-party call reverts NotAccount and the grantee's weight is unchanged.\n- **Cap forfeits withheld emission (medium):** fixed. A capped day pays exactly a 2M curve segment and advances the position only by the consumed loss. In my own 24-window run the claim total was order-independent and conservation held.\n- **Hook deposits revert on backlog (medium):** fixed. 129 deposits followed by a quiet week, then a deposit and a drip, all succeed with conservation intact.\n- **Boost commitment not replaceable, mining state reverts after switch, capped-window gas (low):** fixed and confirmed by scratch tests.\n- **Boost day preparation cost and stalled-successor liveness (low):** the author's disputes are reasonable. Neither is a loss of funds, and the README documents both as accepted operational constraints, so I dropped them.\n\n**One new finding, medium, introduced by the fixes.** A dormant-boost sweep that lands after a capped day has been cached re-prices that day's unclaimed windows on the extended curve while the cached denominator still holds base-curve values. In my reproduction the day pays 2,066,844.9 HAMSTER against a 2M allowance, and bob's window pays more than alice's identical window because he claimed after the sweep. This breaks the cap and claim-time independence. It needs a switch between day 351 and day 365 after deployment, which is realistic given the unknown migration date. The proof is attached as a failing Foundry test. The fix is to price a cached day with the curve state that produced its cached raw sum.\n\n**One info note.** Halvings now follow the cap-adjusted effective loss rather than raw cumulative loss. This is the documented consequence of the deferral design and is listed only for the requester to confirm.\n\nThe coverage record answers all 34 entry points plus one invariant row. The shipped ABI matches the compiled one, the manifest is schema-valid with TEAM as `$owner`, and the full suite passes 83 of 83 tests.","treeHash":null,"usage":{"cachedInputTokens":1452400,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":32215,"runtime":"claude","turns":25,"wallClockMs":618647}},{"artifacts":[],"attempt":1,"bundleHash":"68f4fd57a7376e543cb5d98c15173846958797edaf8404c53946df9d0a2c9d42","device":"4be5e039e1c77ce3","findings":[],"hash":"a9f3e3dc0dbcd5624930372868cc58b4a482207d5303aec5ccdfefb5c11f1969","nodeId":"d204aba9-3cf6-4045-92a8-b81ab18e20a2","outcome":"completed","summary":"Verification is done: the committed launch.json already matches the accepted tree, so I'm finishing without rewriting it.\n\nThe manifest at `launch.json` is correct and unchanged. The earlier rejection was an upload failure, not a content fault, so the right action is to submit the same manifest again.\n\nWhat I verified against the accepted tree:\n\n- **Contracts and order.** The tree has exactly two deployable contracts, HamsterBoost and HamsterVault. HamsterBoost has no constructor, so its argument list is empty. HamsterVault takes two addresses in declaration order, team_ and boostEngine_, written as `$owner` and `$contract:HamsterBoost`. The exported ABI under `abi/` agrees.\n- **Owner handling.** The only privileged role, TEAM, resolves to `$owner`. Neither constructor touches msg.sender or tx.origin, and neither makes an external call, so the protected rehearsal on an empty chain passes.\n- **Schema shape.** Top-level keys are kind, contracts and notes only. Every entry has only contract and constructorArgs. Notes are a single string of 1,685 characters. No odd-length hex strings.\n- **Notes accuracy.** Every function the notes name exists in the source or in the v2 game reference, including scheduleStakingRecipient and executeStakingRecipient on the game.\n- **Build and tests.** `forge build --offline --sizes` and `forge test --offline` pass with the pinned fuzz seed. All 58 tests across 6 suites pass. HamsterVault runtime is 24,421 bytes and its initcode 25,464 bytes, both within the EIP-170 and EIP-3860 limits.\n\nNo file other than launch.json is part of this step, and I changed none. The manifest is already committed in the tree head, so the diff is empty by design, which is the accepted outcome for a manifest step whose manifest is right.","treeHash":"55ba95b5f8b6d94b84d9198f6f9365a5410a8f5a","usage":{"cachedInputTokens":358204,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":3818,"runtime":"claude","turns":7,"wallClockMs":107634}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c0fc4ea4f50e3380","findings":[{"citation":"resolved","description":"The brief says that if boost is not activated within 365 days 'anyone can move the 50M into mining once'. HamsterVault.sweepDormantBoost() (src/HamsterVault.sol:57, `miningAllocation += BOOST_ALLOCATION;`) only bumps the storage variable miningAllocation. Nothing reads that variable on the emission path: _prepareWindow still bounds commitments by the constant MINING_ALLOCATION (750M) and HamsterBoost.emissionCurve saturates at exactly MINING_ALLOCATION (`if (h >= 90) return MINING_ALLOCATION;`). After a sweep the vault therefore holds 50M HAMSTER that no window budget can ever include; bucketsLeft() reports it as miningUncommitted forever and hamsterLiability() carries it as an ungranted reserve with no exit. README section 'Squeek boost' documents this as a deliberate interpretation ('the extra reserve therefore remains locked'), but it is a 50M HAMSTER (5% of supply) permanent lock that contradicts the requested behaviour, and the contract is immutable so it cannot be corrected later. Seam: boundary (post-sweep state) x invariant (miningAllocation - miningCommitted is supposed to be mineable). If the lock is intended, the view bucketsLeft().miningUncommitted is still wrong by 50M; if it is not, the sweep needs to extend the emission (e.g. scale the remaining curve by 800M/750M from the sweep point, or add a 50M tail band) and the commit check must use miningAllocation.","line":157,"path":"src/HamsterMining.sol","reproduction":"Foundry, mocks from test/helpers/Mocks.sol, chainId 1, deploy HamsterBoost then HamsterVault(TEAM, boost), mint 800M HAMSTER to vault and 200M elsewhere, TEAM bindToken. Then: vm.warp(vault.deployedAt() + 365 days); vault.sweepDormantBoost(); assertEq(vault.miningAllocation(), 800_000_000 ether). game.setLoss(0, alice, 100 * 18_750 ether); game.finalize(1); vault.claimMined(V2, [0], alice) -> grants exactly 750_000_000 ether (curve saturated). Now vault.miningCommitted() == 750_000_000 ether, vault.emissionCurve(type(uint256).max) == 750_000_000 ether, and bucketsLeft() returns miningUncommitted == 50_000_000 ether, miningReserved == 0, boostUncommitted == 0, boostReserved == 0. Any further claimMined on any later window returns budget 0 (F(after)-F(before) == 0), so the 50M reported as uncommitted can never be committed or granted. Expected per brief: the 50M becomes mineable; actual: locked forever. (Ran as test_sweepLocks50M in a scratch test; it passes, i.e. confirms the lock.)","severity":"medium","snippet":"            if (miningCommitted > MINING_ALLOCATION) revert BadLossHistory();","title":"sweepDormantBoost raises miningAllocation to 800M but the curve and the commit check stay at 750M, so the swept 50M is permanently unmineable"},{"citation":"resolved","description":"During the 14-day post-switch period each window's raw budget F(after)-F(before) is scaled by 2M/sum. The curve position still advances by the full raw amount (the next window's `before` equals this window's `after`), while miningCommitted only grows by the scaled amount. The difference is never emitted by any later window and has no other path out of the vault, so it is permanently locked, exactly like the swept boost reserve in finding 1. The brief says mining is 'capped at 2M HAMSTER per UTC day' but does not say the excess is destroyed; the halving curve also advances as if it had been paid, so a popular successor game permanently loses up to (raw - 2M) per day for 14 days. README documents 'Withheld amounts do not restart or shift the curve', so this is a design decision to confirm with the requester rather than an arithmetic error; the arithmetic itself (floor scaling, sum of scaled <= 2M, order independence, day bounds by window start, 14-day bound by window start) was checked and holds.","line":444,"path":"src/HamsterBoost.sol","reproduction":"Existing test test/Mining.t.sol test_newGameDailyCapScalesEveryWindowAndExpires: after the switch, windows `first` and `first+1` of the new game each carry 200 IMD of loss. Raw budgets are 4,000,000 HAMSTER each (20,000 x 200), sum 8M > 2M, so each pays 1,000,000. miningCommitted rises by 2M while the global curve advances by 400 IMD of loss (8M of F). The 6M difference is never committable: every later window's budget starts from F(400 IMD). bucketsLeft().miningUncommitted overstates mineable supply by 6M from then on. Expected: either the withheld 6M is deferred/returned to the bucket, or the brief explicitly accepts destruction; actual: silently locked.","severity":"low","snippet":"        if (sum > NEW_GAME_DAILY_CAP) budget = M.mulDiv(budget, NEW_GAME_DAILY_CAP, sum);","title":"New-game daily cap discards the withheld part of the curve permanently (withheld HAMSTER is neither deferred nor returned to the bucket)"},{"citation":"resolved","description":"windowBudget() for an uncached window inside the 14-day cap loops over every window of that UTC day and calls _rawBudget for each (about 8 external staticcalls plus two emissionCurve evaluations). The daily sum is never cached, so each of the day's 24 windows repeats the same 24 _rawBudget calls on first claim, and HamsterBoost._startDay (line 243, `v.windowBudget(game, k)` for every window of the day) repeats it again for every window when the boost day is prepared. Measured against the test mocks (real v2 view calls are comparable: public mapping getters and pure window times): claimMined of one capped window = 1,006,407 gas; 23 capped windows in one claimMined = 10,390,165 gas; prepareBoostDay(8) first call on a capped day with 24 loss-bearing windows = 11,371,955 gas, before any registration work. A 32-window claim batch for a capped day would be ~14M gas. This fits in a mainnet block today, so it is a cost/liveness concern (boost days must be prepared sequentially, so 14 such days cost roughly 160M gas of permissionless maintenance that nobody is paid for), not a brick. Caching the day sum in the vault's windowAccounting on first computation, or caching raw budgets, removes the quadratic factor.","line":441,"path":"src/HamsterBoost.sol","reproduction":"Scratch Foundry test: setUp as in test/helpers/VaultTestBase.sol; activateBoost/executeBoost with a MockNFT and register tokenId 1 for alice; schedule+execute a MockGame(GENESIS) successor; let day = switchTime / 1 days + 2 and first = (day*1 days - GENESIS + 1 hours - 1)/1 hours; next.setLoss(first+i, alice, 100 ether) for i in 0..23; warp to (day+1)*1 days + 2 hours; finalize both games; vault.resolveGame(next); call vault.prepareBoostDay(8) until boost.status(vault).nextDay == day; then measure gasleft() around vault.prepareBoostDay(8) -> 11,371,955; around vault.claimMined(next, [first], alice) -> 1,006,407; around vault.claimMined(next, [first+1..first+23], alice) -> 10,390,165.","severity":"low","snippet":"            (uint256 raw,) = _rawBudget(vault, game, i);","title":"Capped-period window budgets recompute all ~24 sibling raw budgets per call: ~1M gas per claimed window and ~11M gas per boost day preparation"},{"citation":"resolved","description":"Outside the math area but reported because it breaks a stated guarantee: the brief says 'unvested and vested-unwithdrawn balances count as stake weight'. withdrawVested(account, ids) has no caller check, pays the account, and _removeLot removes exactly the withdrawn amount from the account's weight. A third party can therefore repeatedly push a staker's vested portion out of the vault against their will; to regain the weight the victim must stake() the tokens again and wait WARMUP (24h) during which that amount earns nothing, and pay gas each time. README documents relayability as a feature, but relay-by-anyone is only harmless for withdrawUnstaked and claimRewards (no weight impact); for grants it converts a 'vested-unwithdrawn counts as weight' guarantee into one the account cannot rely on. Minimal fix: require msg.sender == account for withdrawVested (or keep relay but let the account opt in).","line":288,"path":"src/HamsterStaking.sol","reproduction":"test/helpers/VaultTestBase setUp; game.setLoss(0, alice, 1 ether); game.finalize(2); vault.claimMined(V2,[0],alice) -> 20,000 HAMSTER grant to alice; warp 1 day (grant active, weight 20,000e18); vault.depositRewards(700 ether); warp +1 day; vm.prank(carol); vault.withdrawVested(alice, [0]) succeeds although carol is unrelated: alice's grant weight drops by vested(alice,0) = 20,000e18 * 2/7, rewardState().weight drops by the same, and alice can only restore it via stake(), which earns nothing for 24h. Expected: a third party cannot reduce alice's stake weight; actual: it can, every day for the 7-day vesting of every grant.","severity":"low","snippet":"    function withdrawVested(address account, uint256[] calldata ids) external guarded returns (uint256 amount) {","title":"Anyone can call withdrawVested for another account and strip its vested-unwithdrawn stake weight, forcing a fresh 24h warm-up"}],"hash":"acaa2242ae7b9437c8a3767d24a47bac68b36c3e14a4ba5522a7e03ed348b954","nodeId":"e8889a26-34d3-4c85-8e52-24a7116d2c1e","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with 4 findings and 37 coverage rows (all 33 listed entry points plus 3 checked invariants and 1 unreached item). Every snippet was verified to match the tree, and the repository's own 52 tests pass as a baseline.\n\n**What I audited.** The math of the mining curve, window budgets and dust carry, the new-game daily cap, the staking index, stream and drip arithmetic, vesting, the boost two-pass allocation, and every boundary the Pashov Math Precision, Boundary and Numerical Gap guides name. I also confirmed over a public RPC that mainnet IMD has 18 decimals and that the v2 game's window 0 starts at the constant the vault hardcodes, so the halving and rate scale are correct.\n\n**Findings, most severe first:**\n\n1. **Medium, swept boost reserve is unmineable.** After the 365-day sweep, the mining allocation variable becomes 800M, but the commit check in `_prepareWindow` and the emission curve both stay at the 750M constant. The 50M stays locked in the immutable vault forever and the buckets view overstates mineable supply by 50M. The README documents this as a deliberate interpretation, but it contradicts the brief's \"move the 50M into mining\".\n2. **Low, cap withholding is destruction.** Budget withheld by the 2M-per-day post-switch cap is never deferred or returned. The curve advances as if paid, so a popular successor loses that HAMSTER permanently. Documented, but worth confirming with the requester.\n3. **Low, quadratic recomputation in the capped period.** Each uncached capped window recomputes all sibling raw budgets. Measured costs: about 1M gas per claimed window and about 11.4M gas to prepare one boost day. Not a brick at today's block limit, but expensive permissionless maintenance.\n4. **Low, outside my area.** Anyone can call `withdrawVested` for another account, stripping its vested-unwithdrawn weight and forcing a 24h warm-up to restore it. Confirmed: a stranger's call reduced a 20,000 HAMSTER grant weight to about 14,286.\n\n**What held.** The curve is monotone, additive and saturates at exactly 750M. Claim shares round down with exact fractional dust carry and are order independent. The staking index, stream ends in clock units, drip anchoring and account credit carry no precision leaks, and the decay function is monotone over the ranges I fuzzed, so the drip subtraction cannot underflow. Game ranges are consecutive and never overlap. Only the bytecode selector probe against the real mainnet v2 runtime was not exercised.","treeHash":null,"usage":{"cachedInputTokens":1718678,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":61348,"runtime":"claude","turns":30,"wallClockMs":807625}},{"artifacts":[],"attempt":2,"bundleHash":"060ff95e1d949e80edd1df79bccb48faa88ff4b283fd9cfe7ea322a0bef184c8","device":"1731fbfe0c4574fb","findings":[],"hash":"b3847826751c8593052fd1c0687368b49371ce40da6a28ddc2455d48abbba244","nodeId":"d204aba9-3cf6-4045-92a8-b81ab18e20a2","outcome":"completed","summary":"The manifest is written and verified. The previous attempt failed only on upload, not on content, so the deliverable is the same shape as a correct manifest with nothing else touched.\n\n**What launch.json contains**\n\n- `HamsterBoost` first, with no constructor arguments. It is the accounting companion with no owner and no tokens.\n- `HamsterVault` second, with `$owner` as TEAM and `$contract:HamsterBoost` as the boost engine. Those are the only two constructor parameters in the accepted source, in declaration order.\n- Notes of 1,685 characters describing the compiled-in IMD and v2 game addresses, TEAM's three limited powers, and the post-deploy steps.\n\n**Checks**\n\n- The file has exactly the keys kind, contracts and notes, and both names are unique ASCII identifiers.\n- `forge build` succeeds. Runtime sizes are 13,538 bytes for the companion and 23,936 for the vault, both under the EIP-170 limit.\n- `forge test` with the verifier's fuzz seed passes all 52 tests.\n- Both constructors make no external calls, so they rehearse on an empty chain, and neither grants anything to msg.sender or tx.origin.\n- Git status shows launch.json as the only change.","treeHash":"ab9f8b07cbcde8b28c9d0787a359bb540835ccb6","usage":{"cachedInputTokens":262497,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":3112,"runtime":"claude","turns":7,"wallClockMs":77791}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"63458a1bd2b2d376","findings":[{"citation":"resolved","description":"bindToken is the only way to arm the vault (claimMined, claimBoost and stake all call _bound()). It reverts unless balanceOf(vault) equals exactly 800_000_000e18. Launch 2 mints 800M to this vault and makes HAMSTER tradable in the same launch, and the vault has no outflow path before it is bound, so any third party who holds 1 wei of HAMSTER can transfer it to the vault before (or by front-running) TEAM's bindToken transaction. After that the balance is 800M+1, every bindToken call reverts with InvalidAmount, nothing can ever lower the balance, and the 800M HAMSTER plus the whole mining/staking/boost design are permanently dead. The vault already treats post-bind donations as harmless excess (hamsterLiability is computed from the 800M constant), so the fix is to accept balanceOf(vault) >= 800M (and, for the same reason, consider totalSupply >= rather than == if the launch token can burn).","line":56,"path":"src/VaultBase.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\n/// @dev Minimal 1B-supply token: 800M minted to the vault, the remainder to a trader.\ncontract DustToken {\n    uint256 public totalSupply;\n    uint8 public constant decimals = 18;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(address vault, address trader) {\n        totalSupply = 1_000_000_000 ether;\n        balanceOf[vault] = 800_000_000 ether;\n        balanceOf[trader] = 200_000_000 ether;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n}\n\ncontract BindDustTest is Test {\n    address internal constant TEAM = address(0x6CBA4751742F6989fA224B2dC5d5d52fCef98a34);\n\n    function test_oneWeiDonationBeforeBindPermanentlyBricksBinding() public {\n        vm.chainId(1);\n        HamsterBoost boost = new HamsterBoost();\n        HamsterVault vault = new HamsterVault(TEAM, address(boost));\n        address trader = makeAddr(\"trader\");\n        DustToken token = new DustToken(address(vault), trader);\n\n        // Anyone holding 1 wei of HAMSTER sends it to the vault before TEAM's bindToken lands.\n        vm.prank(trader);\n        token.transfer(address(vault), 1);\n\n        // Expected: the vault binds (the 1 wei is an unsolicited donation, which the vault already\n        // tolerates after binding). Actual: InvalidAmount, and no path ever removes the 1 wei, so\n        // the vault can never bind and the 800M HAMSTER are locked forever.\n        vm.prank(TEAM);\n        vault.bindToken(address(token));\n        assertEq(vault.token(), address(token), \"vault must bind with a dust donation present\");\n    }\n}","reproduction":"State: HamsterVault deployed, token (1e27 supply, 18 decimals) holding 800_000_000e18 at the vault, trader holds some HAMSTER. 1) trader: token.transfer(vault, 1). 2) TEAM: vault.bindToken(token). Expected: vault.token() == token (dust is an unsolicited donation). Actual: revert InvalidAmount() at VaultBase.sol:54-57; repeats forever since no function moves HAMSTER out of an unbound vault. Proof test test/scratch/BindDust.t.sol fails on the current code with InvalidAmount() and passes once the check is >= 800M.","severity":"high","snippet":"                || IToken(token_).balanceOf(address(this)) != 800_000_000 ether || IToken(token_).decimals() != 18","title":"bindToken requires an exact 800M balance, so 1 wei of HAMSTER sent to the vault before binding locks the vault forever"},{"citation":"resolved","description":"The brief says that if the boost is not activated within 365 days anyone can move the 50M into mining once. sweepDormantBoost marks the boost swept and adds 50M to miningAllocation, but no emission path reads miningAllocation: every window budget is a difference of HamsterBoost.emissionCurve, which saturates at MINING_ALLOCATION = 750M (emissionCurve(type(uint256).max) == 750_000_000e18), and _prepareWindow reverts when miningCommitted exceeds the 750M constant (src/HamsterMining.sol:157 `if (miningCommitted > MINING_ALLOCATION) revert BadLossHistory();`). So after the sweep bucketsLeft() reports miningUncommitted = 800M - committed while at most 750M can ever be committed; the 50M is permanently locked in the vault with no claim, grant, stake or rescue path, and the site view overstates what is left to mine by 50M. Either the sweep must extend the emission schedule (e.g. scale the remaining curve or add a tail band so the integral reaches 800M) or the brief's requirement is unmet and bucketsLeft should not report the 50M as mineable.","line":57,"path":"src/HamsterVault.sol","reproduction":"State: vault deployed, boost never activated. 1) warp to deployedAt + 365 days. 2) anyone: vault.sweepDormantBoost(). 3) read vault.bucketsLeft(): miningUncommitted == 800_000_000e18 (observed in test/scratch/Probe.t.sol test_sweepMovesNothingMineable), vault.miningAllocation() == 800M, but boost.emissionCurve(type(uint256).max) == 750M and vault.MINING_ALLOCATION() == 750M. Expected: 50M becomes mineable. Actual: sum of all window budgets over any loss history is bounded by 750M and _prepareWindow reverts above it, so miningGranted can never exceed 750M and 50M HAMSTER stays in the vault forever.","severity":"medium","snippet":"        miningAllocation += BOOST_ALLOCATION;","title":"sweepDormantBoost only raises a counter: the swept 50M can never be emitted because the curve and the commit check are fixed at 750M"},{"citation":"resolved","description":"prepare() walks every registered NFT twice per UTC day. In the first pass each eligible registration writes a fresh storage slot eligibleCount[vault][day][account][j] for each of up to 25 windows (about 22k gas each), and in the second pass it makes one lossOf external call plus a mulDiv per window. Measured with the repository mocks (test/scratch/Probe.t.sol test_prepareGasPerRegistration): 64 registrations with losses in all 25 windows of a day need 16 prepareBoostDay(8) calls and 36.5M gas for one day, i.e. ~570k gas per registration per day; the real v2 lossOf is heavier than the mock. maxRegistrations is capped at 8 (line 256), days must be prepared chronologically (s.nextDay) and claims for a day need it complete, and nobody is paid for preparing. With 1,000 registered Squeeks a single day costs ~570M gas (about 19 full blocks, several ETH at modest gas prices) and ~250 transactions, every day, forever. If preparers stop, every later boost day is unclaimable although the brief requires boost mining to be deterministic and claimable; the global daily cap (100k) also means the whole collection competes for the same budget, so the per-registration cost cannot be justified by the per-registration reward. Consider snapshotting per-wallet eligible counts once per day instead of per window, bounding the per-day work by wallets rather than NFTs, or raising the per-call limit.","line":272,"path":"src/HamsterBoost.sol","reproduction":"State: boost activated with a mock ERC-721, 64 holders each register one tokenId, every holder has lossOf = 1e18 in all 25 windows of the first prepared day. 1) warp to the end of that day + 3h, game.finalize(20000). 2) call vault.prepareBoostDay(8) until days_(vault, day).complete. Observed: 16 calls, 36,511,523 gas total, 570,492 gas per registration (test/scratch/Probe.t.sol). Expected: per-day preparation cost that a single volunteer can sustain. Actual: cost grows linearly in registered NFTs times windows per day, with no incentive and a hard 8-registration per-call bound.","severity":"medium","snippet":"                        ++eligibleCount[msg.sender][day][r.account][j];","title":"Boost day preparation costs ~0.57M gas per registered NFT per day and must be paid by an unrewarded caller in strict day order, so the boost stalls at modest collection sizes"},{"citation":"resolved","description":"Every state-changing entry point first runs _checkpoint(MAX_CHECKPOINT = 128) and reverts if more than 128 warm-up/stream events lie between the stored wallTime and now. Each stake() in a distinct block creates one Activation 24h later. An attacker who stakes 1 wei in 129 consecutive blocks (measured 32.97M gas in total, about 0.33 ETH at 10 gwei; test/scratch/Backlog.t.sol) creates 129 events that all mature within a 26-minute span one day later. If no vault transaction lands during that span (a quiet pool), the next depositRewards, depositDrip, forwardJackpot-independent but every _ready/_readyGlobal path, stake, claimMined, claimRewards and withdraw call reverts with CheckpointRequired until someone calls checkpoint(128) once more. depositRewards/depositDrip are the calls the launch-2 hook makes inside swaps; unless the hook wraps them in try/catch, swaps on the HAMSTER pool revert for that period, and the attack can be repeated for the cost above. Recovery is permissionless and cheap, so this is a griefing/liveness issue, not a loss; it should be documented for the hook author (catch reverts, or let deposit paths accept a stale clock by streaming from the stored clock) and the README should state the recovery procedure.","line":206,"path":"src/HamsterStaking.sol","reproduction":"State: bound vault, alice holds HAMSTER. 1) For i in 0..128: warp +12s, alice.stake(1). 2) warp +24h+1s with no other vault call. 3) anyone: vault.depositRewards(1e18) -> revert CheckpointRequired(); vault.depositDrip(1e18) -> revert CheckpointRequired(). 4) vault.checkpoint(128) returns false, a second checkpoint(128) returns true, then depositRewards succeeds. Expected: the hook's deposit never depends on a third party catching the clock up. Observed in test/scratch/Backlog.t.sol.","severity":"low","snippet":"        if (!_checkpoint(MAX_CHECKPOINT)) revert CheckpointRequired();","title":"A 129-block burst of 1-wei stakes makes depositRewards/depositDrip (the hook's per-swap calls) revert with CheckpointRequired after any 24h-later idle gap"},{"citation":"resolved","description":"executeGame registers the successor with rangeReady = false and makes it currentGame immediately. currentMiningState(vault) reads games(currentGame) and reverts when the range is not resolved. Resolution needs the previous game finalized up to its end (the last window starting before the switch ends up to 1h later and v2 needs up to TIMEOUT = 48h to finalize an unsettled window) and the new game finalized up to its first counted window, so the view required by the brief (rate and halving) reverts for hours to days after every switch instead of reporting the last known global loss. The data to answer is available: the previous game's finalized prefix plus its globalOrigin give a lower bound, and the view could return that (or the previous game's state) while rangeReady is false.","line":385,"path":"src/HamsterBoost.sol","reproduction":"State: v2 current, successor game scheduled. 1) warp 48h, anyone: vault.executeGame(). 2) immediately call vault.currentMiningState(). Expected: (globalLoss, rate, halving) based on the finalized history so far. Actual: revert NotReady() until resolveGame(successor) succeeds, which requires v2.nextFinalizeWindow() >= v2.end and successor.nextFinalizeWindow() >= successor.first.","severity":"low","snippet":"        if (!g.rangeReady) revert NotReady();","title":"currentMiningState() reverts NotReady from the moment executeGame runs until resolveGame succeeds, breaking the site's rate/halving view"}],"hash":"e155026de4fc3e077c486c4ebc1e92ca6a41f3d1ef2786aa943dcfff85e4352e","nodeId":"548d2d4a-15bb-48c0-bdb4-eb4f6cc80872","outcome":"completed","summary":"All five snippets match the tree, the proof test fails on the current code for the stated reason, and the coverage record answers all 33 entry points plus three invariant rows. Review complete.\n\n**Outcome:** `.imd-findings.json` holds 5 findings and 36 coverage rows. Nothing in the repository was modified; scratch tests live only under `test/scratch/`.\n\n**Findings, most severe first**\n\n1. **High. `bindToken` demands an exact 800M balance.** Anyone holding 1 wei of HAMSTER can send it to the vault before TEAM binds. Binding then reverts forever, and no path moves HAMSTER out of an unbound vault, so the 800M and the whole design are locked. The proof test `test/scratch/BindDust.t.sol` fails today with `InvalidAmount` and passes once the check becomes \"at least 800M\". File `src/VaultBase.sol`, line 56.\n\n2. **Medium. The dormant-boost sweep moves nothing mineable.** The sweep adds 50M to a counter, but every budget comes from a curve saturating at 750M and the commit check uses the 750M constant. After a sweep the bucket view reports 800M uncommitted while 50M can never be emitted.\n\n3. **Medium. Daily boost preparation is too expensive to sustain.** Measured with the repository mocks: 64 registrations need 16 calls and 36.5M gas for one day, about 570k gas per NFT per day, paid by an unrewarded caller in strict day order with an 8-per-call cap.\n\n4. **Low. Activation-burst griefing.** 129 one-wei stakes in consecutive blocks (33M gas total) make `depositRewards` and `depositDrip` revert with `CheckpointRequired` after a quiet day. The hook's per-swap calls would fail until someone calls `checkpoint` twice. Confirmed in `test/scratch/Backlog.t.sol`.\n\n5. **Low. `currentMiningState` reverts after every game switch** until the successor's range resolves, which can take days, breaking the rate and halving view the site needs.\n\n**Coverage.** All entry points were traced for checks, state changes and failure paths. Staking clock, stream ordering, lot sync, mining origins, range exclusion, the 14-day cap determinism, forwarding and flush paths, and the `mulDiv` library all hold. The static-analysis leads (caret operator, weak PRNG, reentrancy) were checked and are false positives for this code. One trust assumption is recorded rather than reported: a TEAM-selected successor that never finalizes its first counted window would block all further switches.\n\n**Not reached.** Live mainnet verification of the v2 genesis constant and the IMD token's transfer semantics, since no RPC was reachable from this machine. The vendored v2 source matches the constant.","treeHash":null,"usage":{"cachedInputTokens":1369366,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":57780,"runtime":"claude","turns":37,"wallClockMs":930246}},{"artifacts":[],"attempt":1,"bundleHash":"bd541fb760f7e50488db6ff3e9d1b4a4c8a35c3cd5cbbffcca78f0625c605034","device":"2a4ad9be53991100","findings":[],"hash":"f4bab37b10c3ae2538b51cae6182e6d1a51ad10371ed2bccfe3fc85bc903066d","nodeId":"4926e1aa-4b0b-40a9-a778-2e53bf2203f2","outcome":"completed","summary":"All 84 tests pass (`forge build` clean, `forge test` with the verifier seed, 10 suites). I found no new defects, so there is no `.imd-findings.json`.\n\n**What changed since the last round:** the implementation author fixed the bug where the 365-day dormant-boost sweep, landing inside a new game's 14-day cap period, could push an already-computed capped day past its allowance. The fix lives in `src/HamsterBoost.sol`. My existing tests still pass on the new code.\n\n**What I added:** `test/SweepCapInvariants.t.sol`, an invariant suite that exercises that fix with random call sequences.\n- **Setup:** the vault is deployed 360 days before v2 genesis, so the sweep opens at genesis + 5 days 1 hour. At that point the first successor game (switched in at genesis + 50h) is still capped. A unit test, `test_sweepOpensInsideFirstCapPeriod`, confirms this, so the property is not checking an empty case.\n- **Random actions:** adding loss (enough to push days over the 2M cap), advancing time, computing capped days in batches of random size, resolving games, claims weighted toward windows that actually carry loss, read-only budget checks on windows in already-computed days, the sweep (only once it opens, without jumping the clock), and an optional second game switch.\n- **What it checks after every sequence:**\n  - A capped day never pays more than 2M, or more than its stored allowance.\n  - A window in an already-computed day pays the same before and after the sweep.\n  - What a claim pays matches what `claimable` showed.\n  - The amount paid out matches what the test tracked independently.\n  - Total committed never exceeds the mining bucket, which is 750M, or 800M after the sweep.\n  - The vault always holds exactly the HAMSTER it owes.\n  - The dormant boost never commits or pays anything.\n  - Nothing is committed beyond the emission curve.\n\n**Checking the test itself:** I temporarily put back the pre-fix `HamsterBoost.sol`. The new invariant failed with \"capped day above 2M\", shrunk to a 6-call sequence: add loss, switch, advance, claim, sweep, claim. I then restored the current file; `git status` shows only the new test file. My first two versions of the handler passed on the buggy code, because the sweep fired before any capped day had been computed. I fixed that before relying on it. On the current code the suite passes with the verifier seed and three other seeds.\n\nI did not commit anything.","treeHash":"f68c1acb1c3aa8a7c508229d5dacdc493ec04b0d","usage":{"cachedInputTokens":919023,"inputTokens":36,"model":"claude-opus-5-5","outputTokens":20964,"runtime":"claude","turns":19,"wallClockMs":578013}},{"artifacts":[],"attempt":1,"bundleHash":"b208b9f749bf14f7e7dfbce397bccbe41a959289e74bf3956d1f77dc309e1d9b","device":"dbfcf48f94822fd7","findings":[],"hash":"fb92d01b164e0ebdde6c25045a6960ff1c85099590a162ed5e274908b0c2e3f7","nodeId":"d204aba9-3cf6-4045-92a8-b81ab18e20a2","outcome":"completed","summary":"The manifest is in place and verified, with nothing else to change.\n\n**Outcome:** launch.json already holds the accepted manifest from my previous round. The rejected attempt failed only on bundle upload, so the content needed no repair. I re-verified it against the current tree, since the builder changed HamsterBoost after my last round.\n\n**What I checked**\n- HamsterBoost still has no constructor, and HamsterVault still takes exactly (team, boostEngine). The manifest lists them in that dependency order with `$owner` and `$contract:HamsterBoost`.\n- Schema: only kind, contracts and notes are present, all arguments are strings, names are unique identifiers, and notes are under the 4,000 character limit.\n- Both constructors make no external calls, so they rehearse on the empty chain the protected probe uses. No selfdestruct, delegatecall or callcode in the source.\n- Runtime sizes fit the EIP-170 limit the probe enforces. HamsterVault is close at 155 bytes of margin, but the accepted source is not mine to change.\n- Build succeeds, and all 59 tests pass with the verifier's fuzz seed.\n- Git status is clean: no file other than the manifest differs from the accepted tree.\n\n**Next:** the harness submits launch.json as is. After deploy, TEAM calls scheduleStakingRecipient(vault) then executeStakingRecipient on the v2 game, as the notes record.","treeHash":"df5604c646a91abf2ac2fe5d41db18ef669ae3c1","usage":{"cachedInputTokens":252400,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":3211,"runtime":"claude","turns":7,"wallClockMs":156219}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"401c4383398af47c","findings":[{"citation":"resolved","description":"Access x asymmetry trust gap. bindToken is the only path that sets `token`, is onlyTeam and can succeed once, and requires the vault's HAMSTER balance to equal exactly 800,000,000e18. Launch 2 mints 1B, delivers 800M to the vault and puts the rest in a Uniswap v4 pool, so from the launch-2 transaction onward any trader holds HAMSTER. Transferring 1 wei to the vault (a plain ERC-20 transfer the vault cannot refuse) makes balanceOf(vault) = 800M + 1 wei, so bindToken reverts InvalidAmount for TEAM, and there is no rescue, burn or sweep path (`hamsterLiability` comment: donations have no rescue path). Nothing else can ever set `token`: claimMined, claimBoost, stake, withdrawVested and withdrawUnstaked are gated by _bound() and the 800M allocation is locked forever; IMD pulled by pullGameRewards/depositRewards/depositDrip streams to zero weight forever. The asymmetry: after bind the vault explicitly tolerates donations as excess, but before bind a 1 wei donation is fatal. Cost to the attacker is one transfer; the window is every block between launch 2 and TEAM's bind tx, and the bind tx itself can be front-run in the mempool. Preconditions: launch 2 deployed; TEAM has not yet bound. Minimal fix that preserves the brief's intent (vault must hold the full 800M remainder): require `balanceOf(vault) >= 800_000_000 ether` (and keep totalSupply == 1e27 and decimals == 18), treating any excess as the same unrescuable donation the vault already accepts post-bind. This is a one-token change to the brief's literal `== 8e26`; the requester must confirm that tradeoff.","line":56,"path":"src/VaultBase.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\n/// @dev Minimal 18-decimal ERC-20 standing in for the launch-2 HAMSTER token.\ncontract Hamster {\n    string public constant name = \"HAMSTER\";\n    uint8 public constant decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @notice Finding: any HAMSTER holder can permanently block TEAM's one-shot bindToken by sending\n/// 1 wei of HAMSTER to the vault before TEAM binds. bindToken requires balanceOf(vault) == 8e26\n/// exactly, there is no rescue path for donations, and nothing else in the vault can ever set\n/// `token`, so the 800M allocation, mining, staking and boost claims are bricked forever.\ncontract BindGriefTest is Test {\n    address internal constant TEAM = address(bytes20(hex\"6cba4751742f6989fa224b2dc5d5d52fcef98a34\"));\n    HamsterVault internal vault;\n    Hamster internal hamster;\n    address internal sniper = makeAddr(\"sniper\");\n\n    function setUp() public {\n        vm.chainId(1);\n        HamsterBoost boost = new HamsterBoost();\n        vault = new HamsterVault(TEAM, address(boost));\n        hamster = new Hamster();\n        // Launch 2: 1B supply, exactly 800M to the vault, 200M to the pool (here: a buyer).\n        hamster.mint(address(vault), 800_000_000 ether);\n        hamster.mint(sniper, 200_000_000 ether);\n    }\n\n    function test_oneWeiDonationBeforeBindBricksVaultForever() public {\n        // A trader who bought any HAMSTER front-runs TEAM's bindToken with a 1 wei transfer.\n        vm.prank(sniper);\n        hamster.transfer(address(vault), 1);\n\n        // The honest launch state: correct token, correct supply, the full 800M delivered.\n        assertEq(hamster.totalSupply(), 1_000_000_000 ether);\n        assertGe(hamster.balanceOf(address(vault)), 800_000_000 ether);\n\n        // Expected: TEAM can still bind the genuine token (excess is a donation, as everywhere\n        // else in the vault). Actual: bindToken reverts InvalidAmount, now and forever.\n        vm.prank(TEAM);\n        vault.bindToken(address(hamster));\n        assertEq(vault.token(), address(hamster), \"vault must bind the genuine launch-2 token\");\n    }\n}","reproduction":"State: HamsterBoost and HamsterVault(TEAM, boost) deployed; launch-2 token with totalSupply 1e27, decimals 18, 800e24 minted to the vault and 200e24 held by a trader. Sequence: (1) trader calls hamster.transfer(vault, 1); (2) TEAM calls vault.bindToken(hamster). Expected: bind succeeds (the genuine token with the full allocation delivered) and vault.token() == hamster. Actual: revert InvalidAmount() at VaultBase.sol:54-57 because balanceOf(vault) == 800_000_000e18 + 1; every later bindToken reverts the same way, so token stays address(0) forever. Test: test/scratch/BindGrief.t.sol fails on this tree with `[FAIL: InvalidAmount()]`.","severity":"high","snippet":"                || IToken(token_).balanceOf(address(this)) != 800_000_000 ether || IToken(token_).decimals() != 18","title":"Any HAMSTER holder can permanently brick the vault by sending 1 wei before TEAM's one-shot bindToken (strict balance equality)"},{"citation":"resolved","description":"Access x economics x asymmetry. The brief says unvested AND vested-but-unwithdrawn grant balances count as stake weight, i.e. a grantee chooses when to pull vested principal out of the weight pool. withdrawVested(account, ids) can be called by anyone for any account (README calls it 'relayable'), and it calls _removeLot which subtracts the withdrawn amount from _reward.weight and accounts[account].weight (or from the pending activation during warm-up). The asymmetry: stake() and requestUnstake() act only on msg.sender, but the economically equivalent weight-removal path for grants is open to everyone. A competing staker therefore pushes every other grantee's vested slice out of the pool the moment it vests (linear vesting vests every second, so this can be repeated each block for the full 7 days), raising the attacker's pro-rata share of every IMD stream and drip. The victim's only remedy is to re-stake the pushed-out tokens, which costs gas and a fresh 24h warm-up each time, during which it earns nothing; the attacker can repeat the push as soon as any new amount vests. Concrete delta with two equal participants (victim grant G, attacker stake G): after day 1 the attacker removes G/7 of the victim's weight, so the victim's share of all rewards falls from 50% to 6/13 = 46.2% and keeps falling to 50% of (unvested only) over the week; the attacker gains the difference. Minimal fix preserving the design: require msg.sender == account in withdrawVested (and, for symmetry, in withdrawUnstaked/claimRewards if third-party relaying is not wanted), or let the account opt in to relayers.","line":288,"path":"src/HamsterStaking.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\ncontract Token {\n    uint8 public constant decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev Minimal v2-shaped game: one account loses in window 0, finalized on demand.\ncontract Game {\n    address public constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    uint64 public constant genesis = 1791847200;\n    uint256 public nextFinalizeWindow;\n    uint256 public cumulative;\n    mapping(uint256 => mapping(address => uint256)) public lossOf;\n    mapping(uint256 => uint256) public totalEligibleLoss;\n    mapping(uint256 => uint256) public cumulativeLossBefore;\n    mapping(uint256 => uint256) public cumulativeLossAfter;\n    mapping(address => uint256) public balances;\n\n    function windowStart(uint256 k) external pure returns (uint64) {\n        return uint64(genesis + k * 1 hours);\n    }\n\n    function windowEnd(uint256 k) public pure returns (uint64) {\n        return uint64(genesis + (k + 1) * 1 hours);\n    }\n\n    function setLoss(uint256 k, address who, uint256 amount) external {\n        totalEligibleLoss[k] = totalEligibleLoss[k] - lossOf[k][who] + amount;\n        lossOf[k][who] = amount;\n    }\n\n    function finalize(uint256 maxWindows) external {\n        for (uint256 i; i < maxWindows; ++i) {\n            uint256 k = nextFinalizeWindow;\n            if (windowEnd(k) > block.timestamp) break;\n            cumulativeLossBefore[k] = cumulative;\n            cumulative += totalEligibleLoss[k];\n            cumulativeLossAfter[k] = cumulative;\n            ++nextFinalizeWindow;\n        }\n    }\n\n    function withdraw() external {}\n    function depositJackpot(uint256) external {}\n    function fundOracleReserve(uint256) external {}\n}\n\n/// @notice Finding: withdrawVested(account, ids) has no caller check, so any competing staker can\n/// force-withdraw another holder's vested-but-unwithdrawn grant principal every block. The brief\n/// says vested-unwithdrawn balances count as stake weight; the attacker strips that weight from\n/// the victim (24h warm-up to re-stake) and raises its own pro-rata IMD share.\ncontract VestedGriefTest is Test {\n    address internal constant TEAM = address(bytes20(hex\"6cba4751742f6989fa224b2dc5d5d52fcef98a34\"));\n    address internal constant IMD = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    address internal constant V2 = 0xe33AB493249a2Df0D8c4a71aC54F46d1F7778367;\n    uint64 internal constant GENESIS = 1791847200;\n    HamsterVault internal vault;\n    Token internal hamster;\n    Game internal game;\n    address internal alice = makeAddr(\"alice\");\n    address internal bob = makeAddr(\"bob\");\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.warp(uint256(GENESIS) + 2 hours);\n        vm.etch(IMD, address(new Token()).code);\n        vm.etch(V2, address(new Game()).code);\n        game = Game(V2);\n        HamsterBoost boost = new HamsterBoost();\n        vault = new HamsterVault(TEAM, address(boost));\n        hamster = new Token();\n        hamster.mint(address(vault), 800_000_000 ether);\n        hamster.mint(bob, 200_000_000 ether);\n        vm.prank(TEAM);\n        vault.bindToken(address(hamster));\n        vm.prank(bob);\n        hamster.approve(address(vault), type(uint256).max);\n    }\n\n    function test_thirdPartyCannotStripVestedStakeWeight() public {\n        // Alice lost 100 IMD in window 0; her retroactive claim creates a 7-day grant.\n        game.setLoss(0, alice, 100 ether);\n        game.finalize(1);\n        uint256[] memory w = new uint256[](1);\n        uint256 granted = vault.claimMined(V2, w, alice);\n        assertGt(granted, 0);\n        // Bob stakes the same amount; both lots warm up together.\n        vm.prank(bob);\n        vault.stake(granted);\n        vm.warp(block.timestamp + 24 hours);\n        vault.checkpoint(128);\n        vault.checkpointAccount(alice, 128);\n        (uint256 aliceWeightBefore,,,,) = vault.accounts(alice);\n        assertEq(aliceWeightBefore, granted, \"grant counts as stake weight once warmed up\");\n\n        // Bob, not Alice, withdraws Alice's vested slice (1/7 after one day).\n        uint256[] memory ids = new uint256[](1);\n        vm.prank(bob);\n        (bool ok,) = address(vault).call(abi.encodeCall(vault.withdrawVested, (alice, ids)));\n        (uint256 aliceWeightAfter,,,,) = vault.accounts(alice);\n\n        // Expected: a stranger cannot change Alice's position; her vested-unwithdrawn balance keeps\n        // earning until she withdraws it herself. Actual: the call succeeds and Alice's weight drops\n        // by granted/7 while Bob's share of every IMD stream rises accordingly.\n        assertTrue(!ok || aliceWeightAfter == aliceWeightBefore, \"third party stripped vested stake weight\");\n    }\n}","reproduction":"State: token bound; v2 window 0 finalized with lossOf(0, alice)=100e18 (totalEligibleLoss 100e18). Sequence: (1) anyone calls claimMined(V2,[0],alice): alice gets a 7-day grant of ~2,000,000e18 HAMSTER in lot 0 with 24h warm-up; (2) bob stakes the same amount; (3) warp +24h, checkpoint(128), checkpointAccount(alice,128): accounts(alice).weight == granted; (4) bob calls withdrawVested(alice,[0]). Expected: revert (bob is not alice) and alice's weight unchanged. Actual: call succeeds, granted/7 HAMSTER is pushed to alice's wallet and accounts(alice).weight drops to 6/7 of granted; _reward.weight drops by the same amount, so bob's share of every stream rises. Test: test/scratch/VestedGrief.t.sol fails on this tree with `[FAIL: third party stripped vested stake weight]`.","severity":"medium","snippet":"    function withdrawVested(address account, uint256[] calldata ids) external guarded returns (uint256 amount) {","title":"withdrawVested has no caller check: any competing staker can strip another holder's vested-unwithdrawn stake weight every block"},{"citation":"resolved","description":"Asymmetry between writer and readers of miningAllocation. sweepDormantBoost raises miningAllocation to 800M, but the only readers of that variable are the bucketsLeft view and hamsterLiability; the budget cap in _prepareWindow (src/HamsterMining.sol:157 `if (miningCommitted > MINING_ALLOCATION) revert BadLossHistory();`) and the emission curve (HamsterBoost.emissionCurve saturates at MINING_ALLOCATION = 750M) still use the 750M constant. So after the sweep no window can ever commit any of the extra 50M: the brief's 'anyone can move the 50M into mining once' becomes 'the 50M is reported as mining supply but is unclaimable by anyone, forever'. The README documents this interpretation ('remains locked under this immutable curve') and asks the independent review to examine it; reporting it here because the outcome is a permanent lock of 5% of total supply with no beneficiary, which is a loss of funds under a specific condition (boost never activated within 365 days). Fix needs a scope decision by the requester: either extend the curve so it converges to miningAllocation after the sweep (e.g. scale the remaining bands by 800/750 from the sweep's global-loss point, so halvings never reset), or keep the curve and make the sweep a no-op on allocation so the views stop reporting unreachable supply.","line":57,"path":"src/HamsterVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {HamsterVault} from \"src/HamsterVault.sol\";\nimport {HamsterBoost} from \"src/HamsterBoost.sol\";\n\n/// @notice Finding: sweepDormantBoost \"moves\" the dormant 50M boost bucket into mining by raising\n/// miningAllocation to 800M, but the emission curve and the commit cap in _prepareWindow stay at\n/// the 750M constant, so the 50M can never be committed to any window: it is locked forever.\ncontract SweepLockTest is Test {\n    address internal constant TEAM = address(bytes20(hex\"6cba4751742f6989fa224b2dc5d5d52fcef98a34\"));\n    HamsterVault internal vault;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.warp(1791847200 + 2 hours);\n        HamsterBoost boost = new HamsterBoost();\n        vault = new HamsterVault(TEAM, address(boost));\n    }\n\n    function test_sweptBoostBucketIsReachableByMining() public {\n        vm.warp(vault.deployedAt() + 365 days);\n        vault.sweepDormantBoost();\n        (uint256 miningUncommitted,, uint256 boostUncommitted,) = vault.bucketsLeft();\n        assertEq(boostUncommitted, 0);\n        assertEq(miningUncommitted, 800_000_000 ether, \"bucket view reports 800M available to mining\");\n        // Expected after the brief's sweep: everything reported as uncommitted mining supply can be\n        // emitted by the curve. Actual: the curve saturates at 750M for any loss, so 50M is unreachable.\n        assertEq(\n            vault.emissionCurve(type(uint256).max),\n            miningUncommitted + vault.miningCommitted(),\n            \"curve can never emit the swept 50M\"\n        );\n    }\n}","reproduction":"State: fresh HamsterBoost + HamsterVault(TEAM, boost), boost never activated. Sequence: (1) warp to deployedAt + 365 days; (2) anyone calls sweepDormantBoost(); (3) read bucketsLeft(): miningUncommitted = 800,000,000e18, boostUncommitted = 0; (4) read emissionCurve(type(uint256).max) = 750,000,000e18. Expected: the supply reported as mining-uncommitted is reachable by the curve (800M). Actual: 750M < 800M; _prepareWindow reverts BadLossHistory for any window whose budget would push miningCommitted past 750M, so the 50M is permanently stranded in the vault. Test: test/scratch/SweepLock.t.sol fails with `750000000000000000000000000 != 800000000000000000000000000`.","severity":"medium","snippet":"        miningAllocation += BOOST_ALLOCATION;","title":"sweepDormantBoost moves the dormant 50M into a mining bucket the emission curve can never reach: 50M HAMSTER locked forever"},{"citation":"resolved","description":"Admin-variant asymmetry. scheduleGame lets TEAM replace a pending candidate (restarting the 48h delay), so its timelock gives the public a review window that can lead to a correction. schedule() for the boost reverts AlreadyDone while a pendingCollection exists, and executeBoost() is permissionless after 12h, so once TEAM has committed a wrong address (typo, non-canonical Squeek clone that passes supportsInterface) there is no path to the right one: the 12h timelock is a notification, not a safeguard, and the 50M boost bucket is then bound to the wrong collection until the 365-day sweep is impossible too (sweep requires collection == 0). Preconditions: TEAM error or a compromised TEAM key. Fix: allow TEAM to re-schedule (overwrite pendingCollection and restart readyAt) before execute, mirroring scheduleGame; the one-shot property should bind at execute, not at schedule.","line":139,"path":"src/HamsterBoost.sol","reproduction":"State: chainid 1, two ERC-721 contracts W (wrong) and S (canonical), both returning true for supportsInterface(0x80ac58cd). Sequence: (1) TEAM calls vault.activateBoost(W): Scheduled; (2) TEAM notices and calls vault.activateBoost(S). Expected: pending commitment replaced by S, delay restarts (as scheduleGame does). Actual: revert AlreadyDone() at HamsterBoost.sol:139; (3) after 12h anyone calls executeBoost(): collection = W permanently; activateBoost(S) and sweepDormantBoost() revert AlreadyDone forever.","severity":"low","snippet":"        if (s.collection != address(0) || s.pendingCollection != address(0) || s.swept) revert AlreadyDone();","title":"activateBoost commitment cannot be replaced or cancelled during its 12h timelock, unlike scheduleGame"},{"citation":"resolved","description":"Documented for the judge, not a defect. TEAM (immutable, launch wallet) holds exactly three powers, each guarded by onlyTeam and all traced: bindToken (once; any token passing supply/balance/decimals checks), scheduleGame (48h public delay, replaceable, executed by anyone) and activateBoost (12h delay, once, executed by anyone). No owner, pause, upgrade, rescue or withdrawal path exists; constructors assign nothing to msg.sender or tx.origin. The material trust risk is scheduleGame: checkGame only probes the ABI, so a successor game TEAM controls can (a) report arbitrary lossOf/cumulativeLoss and thereby mint the whole remaining mining curve to TEAM-chosen accounts via permissionless claimMined, and (b) receive all forwarded jackpot/oracle IMD via flushJackpot/flushOracle and keep it. Both are explicitly accepted in the README ('Registered games are trusted'). Liveness note checked: a successor's range resolution needs the old game finalized up to its last counted window; v2's finalize is permissionless and times windows out after TIMEOUT, so an abandoned oracle cannot block resolution. HamsterBoost namespaces all state by msg.sender, so direct calls on it cannot touch the vault's namespace (verified for schedule/execute/sweep/register/invalidate/prepare/claim).","line":89,"path":"src/HamsterMining.sol","reproduction":"Not a defect; no failing input. For the trust risk: TEAM calls scheduleGame(G) where G is a TEAM-controlled contract passing checkGame; after 48h anyone calls executeGame(); G reports lossOf(k, TEAM)=totalEligibleLoss(k) for its windows; claimMined(G, windows, TEAM) grants TEAM the entire remaining 750M curve; flushJackpot sends all pending IMD to G.","severity":"info","snippet":"    function scheduleGame(address candidate) external onlyTeam guarded {","title":"Trust assumptions: TEAM's three powers and what a dishonest successor game can do"}],"hash":"fce7879fb8a22ca339c716900aaf3a189ba04339bda34c7bfdb2cc88ab2662ef","nodeId":"62c4d141-9d18-4884-b5cf-ac203b77b72d","outcome":"completed","summary":"The review is complete. The findings file is at `.imd-findings.json` in the repository root, with 5 findings and a 36-row coverage record covering all 33 listed entry points plus three invariants. No tracked file was changed; the only writes are the findings file and three scratch tests, all git-ignored.\n\n**Findings, most severe first**\n\n1. **High. One-shot bindToken is griefable by anyone.** The vault requires its HAMSTER balance to equal exactly 800M at bind time. After launch 2, any trader holding HAMSTER can transfer 1 wei to the vault, and TEAM's bind reverts forever. There is no rescue path and nothing else sets the token, so the 800M allocation, mining, staking and boost claims are bricked. Proof test fails on this tree with `InvalidAmount()`. The fix is a `>=` check, which changes the brief's literal `==`, so the requester must confirm that tradeoff.\n\n2. **Medium. withdrawVested is callable by anyone for any account.** It removes the victim's vested-unwithdrawn stake weight, which the brief says must keep earning. A competing staker can strip every other grantee's vested slice each block and raise its own IMD share. Proof test fails on this tree.\n\n3. **Medium. sweepDormantBoost locks 50M forever.** The sweep raises the mining allocation to 800M, but the emission curve and commit cap stay at the 750M constant, so the swept 50M is unreachable by any claim. The README documents this reading and asks for review of it. Proof test fails on this tree.\n\n4. **Low. activateBoost cannot be corrected during its timelock**, unlike scheduleGame, so a wrong collection becomes permanent once anyone executes it.\n\n5. **Info. Trust assumptions.** TEAM's three powers and what a dishonest successor game could do are recorded for the judge, not as defects.\n\n**Verification**\n\n- All three proof tests under `test/scratch/` were run with `forge test --match-path` and fail for the stated reasons.\n- Every finding's path, line and snippet were checked programmatically against the tree.\n- The hardcoded v2 genesis, window length and IMD address were confirmed against mainnet via a public RPC.\n\n**Coverage limits**\n\nAll entry points in my area were traced. The reference game's finalize timeout was read to confirm liveness of successor resolution. I did not inspect the live IMD token bytecode for transfer hooks, and I did not fuzz the staking arithmetic, which belongs to another specialist.","treeHash":null,"usage":{"cachedInputTokens":1834258,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":45911,"runtime":"claude","turns":34,"wallClockMs":741835}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"884410e598069409","findings":[],"hash":"fee14f9cea6f717330267e4cfd972dc89a541ce303e711a0b3f6b1266a0bd3dc","nodeId":"d204aba9-3cf6-4045-92a8-b81ab18e20a2","outcome":"failed","summary":"bundle upload failed: fetch failed","treeHash":null,"usage":{"cachedInputTokens":466560,"inputTokens":39127,"model":"gpt-6-astra","outputTokens":4042,"runtime":"codex","turns":3,"wallClockMs":133871}}],"verification":[{"checks":[{"durationMs":119605,"exitCode":0,"name":"build","output":"Compiling 39 files with Solc 0.8.26\nSolc 0.8.26 finished in 119.37s\nCompiler run successful!\n","passed":true},{"durationMs":12553,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2236488)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10208973)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2596095)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 193.09ms (22.47ms CPU time)\n\nRan 15 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2575443, ~: 2576170)\nLogs:\n  Bound result 107984262110738209\n  Bound result 38528336477069297\n  Bound result 30685626785546424996\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 23910, ~: 24173)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158106)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 57774)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 15993848)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 800261)\n[PASS] test_halvingCrossingAndDust() (gas: 2368787)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1225357)\n[PASS] test_newGameDailyCapScalesEveryWindowAndExpires() (gas: 22063789)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1029358)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1013938)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463653)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10259299)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283355)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 12334834)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 195.10ms (324.57ms CPU time)\n\nRan 12 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1406338)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443880)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600203)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 29417421)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24128297)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10460707)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10665513)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 915719)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14762775)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10917378)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 448587)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6067231)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 352.91ms (215.15ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 928308593)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.91s (6.91s CPU time)\n\nRan 16 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12473, ~: 11797)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5894793, ~: 5904307)\nLogs:\n  Bound result 9000000000000086401\n  Bound result 1544278301092260097\n  Bound result 607801\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2087831)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40210691)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1112401)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2242712)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3477799)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1579878)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2675651)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1153356)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1742203)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1476515)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1306878)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1535258)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2396029)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2619236)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 10.03s (816.20ms CPU time)\n\nRan 5 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 770142)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1425890)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 460354)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 579364)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2039336)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 10.35s (7.69ms CPU time)\n\nRan 19 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_newGameDailyCapNeverExceeded(uint256) (runs: 256, μ: 68805303, ~: 68805303)\nLogs:\n  Bound result 1187536261197570718602\n  Bound result 498074683209125369855\n  Bound result 696852233697184121928\n  Bound result 1869739601738441800908\n  Bound result 296233237847262467676\n  Bound result 663882603836416458120\n  Bound result 582437055069703124980\n  Bound result 238418820796982687756\n  Bound result 1044042511041016655682\n  Bound result 986322891476498988473\n  Bound result 530105722109898619798\n  Bound result 1441821755793809260372\n  Bound result 617633938557918181140\n  Bound result 931975530205680680195\n  Bound result 715975663409914846720\n  Bound result 1305476867858805003383\n  Bound result 898389449975640648041\n  Bound result 1228586964067141404008\n  Bound result 209070527673260285146\n  Bound result 36627342709783602294\n  Bound result 1702210229274829221458\n  Bound result 1589709878095114856726\n  Bound result 512499826738830876162\n  Bound result 1502890398303547113066\n  Bound result 607594497845366011519\n  Bound result 1609045263098035556949\n  Bound result 1102457734775296332211\n  Bound result 873990231626725831873\n  Bound result 1101168001144565922697\n  Bound result 875155870092829672584\n  Bound result 1951165247372186867852\n  Bound result 1177251611692925984261\n  Bound result 1429905689467638691829\n  Bound result 1058714992349251146024\n  Bound result 578816774670064232580\n  Bound result 871559615664181728771\n  Bound result 872441531662025919763\n  Bound result 1403760665867896654804\n  Bound result 1910494610758020986303\n  Bound result 101065721661244406280\n  Bound result 1428142976555810857444\n  Bound result 716875027723281233488\n  Bound result 275387533903591519587\n  Bound result 1854426967576849539268\n  Bound result 1128427588487492919838\n  Bound result 1192530429945654861407\n\n[PASS] testFuzz_windowBudgetsTelescopeAcrossHalvings(uint256,uint8) (runs: 512, μ: 5613836, ~: 4560823)\nLogs:\n  Bound result 12\n  Bound result 18478528479963387832655\n  Bound result 14180767782032576685877\n  Bound result 28473415411963538163146\n  Bound result 24491949955616397124075\n  Bound result 6328558006099675415207\n  Bound result 26588581543272911722284\n  Bound result 21266770546439419480583\n  Bound result 1385006534685234171833\n  Bound result 21359386343635160105236\n  Bound result 11323463372095372265056\n  Bound result 19701705089783263029573\n  Bound result 9363527761681382404437\n  Bound result 12132024206884595494739\n  Bound result 1046115075541626841108\n  Bound result 22406045099105283119286\n  Bound result 25910251940974739875639\n  Bound result 29899795836058427562919\n  Bound result 17715086639950550733008\n  Bound result 27641303749938233427242\n  Bound result 4468191566121481801928\n  Bound result 26293968396231512265131\n  Bound result 14182682220020317875895\n  Bound result 3736815034763758101204\n  Bound result 13905604806543179493854\n\n[PASS] test_bindRejectsWrongSupplyBalanceImdAndEoa() (gas: 1268853)\n[PASS] test_boostActivationDeadlineBoundary() (gas: 565282)\n[PASS] test_boostEntryPointsInertBeforeActivation() (gas: 318620)\n[PASS] test_boostRejectsNonMainnetAndNonErc721() (gas: 306138)\n[PASS] test_constructorRejectsZeroTeamOrBoost() (gas: 6813)\n[PASS] test_directBoostCallsCannotTouchVaultNamespace() (gas: 220525)\n[PASS] test_everyTeamPowerRejectsOtherCallers() (gas: 883300)\n[PASS] test_forwardingFailurePaths() (gas: 891067)\n[PASS] test_forwardingFollowsSecondSwitchWithoutHookChange() (gas: 4345358)\n[PASS] test_gameScheduleBoundaries() (gas: 2341050)\n[PASS] test_grantLotCannotBeUnstakedOrWithdrawnEarly() (gas: 1575132)\n[PASS] test_lossAboveTotalOrOverclaimedWindowReverts() (gas: 1324987)\n[PASS] test_rateAndHalvingEdges() (gas: 91789)\n[PASS] test_rewardFailurePaths() (gas: 767061)\n[PASS] test_stakingFailurePaths() (gas: 2058197)\n[PASS] test_unknownGameAndWindowBeyondFinalization() (gas: 1036780)\n[PASS] test_zeroLossClaimConsumesSlotWithoutGrant() (gas: 1096879)\nSuite result: ok. 19 passed; 0 failed; 0 skipped; finished in 10.35s (10.80s CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantTest\n[PASS]\nVaultInvariantTest invariants:\n[PASS] invariant_balancesWeightAndPrincipal\n[PASS] invariant_custodyEqualsLiabilities\n[PASS] invariant_miningBucketAndWindowAccounting\n[PASS] invariant_rewardsNeverExceedDeposits\n VaultInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | addLoss          | 136   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | advance          | 148   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | checkpoint       | 147   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claim            | 124   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claimRewards     | 154   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositDrip      | 120   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositRewards   | 150   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | flush            | 135   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | forward          | 122   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullGame         | 138   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | requestUnstake   | 134   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | stake            | 130   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawUnstaked | 149   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawVested   | 133   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 20590\n  Bound result 94\n  Bound result 5000000000000000001\n  Bound result 10430\n  Bound result 105600\n  Bound result 28142141643096\n  Bound result 94\n  Bound result 2258\n  Bound result 10800\n  Bound result 34560000\n  Bound result 4859440462881695197520\n  Bound result 10\n  Bound result 604800\n  Bound result 128\n  Bound result 1379\n  Bound result 95\n  Bound result 80\n  Bound result 86400\n  Bound result 216\n  Bound result 4108632138625876818661\n  Bound result 127\n  Bound result 8640000\n  Bound result 44\n  Bound result 15673\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 12.22s (12.21s CPU time)\n\nRan 8 test suites in 12.23s (50.61s CPU time): 72 tests passed, 0 failed, 0 skipped (72 total tests)\n","passed":true},{"durationMs":354,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":163,\"abi/HamsterBoost.json\":980,\"abi/HamsterVault.json\":2425,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":30,\"foundry.toml\":18,\"remappings.txt\":1,\"src/HamsterBoost.sol\":446,\"src/HamsterMining.sol\":207,\"src/HamsterStaking.sol\":409,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":275,\"test/BoostExhaustion.t.sol\":110,\"test/EdgeCases.t.sol\":459,\"test/Forwarding.t.sol\":86,\"test/Mining.t.sol\":321,\"test/Staking.t.sol\":343,\"test/V2Integration.t.sol\":125,\"test/VaultInvariants.t.sol\":310,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"5277909933f0d87922a40678e5f18112b1698a38c99846d99218a888d84902ed","verifiedTreeHash":"10bca31b9e2716685ed2c703475acf31a53abbd0","verifierVersion":"0.1.0+475a429a"},{"checks":[{"durationMs":29347,"exitCode":0,"name":"build","output":"Compiling 37 files with Solc 0.8.26\nSolc 0.8.26 finished in 29.27s\nCompiler run successful!\n","passed":true},{"durationMs":891,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2280518)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10222585)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2622317)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 7.17ms (7.03ms CPU time)\n\nRan 6 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_bindAcceptsDonatedExcessButNotShortfallOrWrongSupply() (gas: 1686809)\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 777331)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1433019)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 467558)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 586591)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2046557)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 59.95ms (4.43ms CPU time)\n\nRan 18 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2682565, ~: 2683278)\nLogs:\n  Bound result 23459\n  Bound result 669136356\n  Bound result 12631\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 54034, ~: 54305)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158160)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 116271)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 18332582)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 834361)\n[PASS] test_halvingCrossingAndDust() (gas: 2457012)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1288540)\n[PASS] test_miningStateFallsBackToOutgoingGameWhileSuccessorUnresolved() (gas: 1341049)\n[PASS] test_newGameCapCrossingHalvingKeepsCurveConsistentAndChainsToNextGame() (gas: 13951820)\n[PASS] test_newGameDailyCapScalesEveryWindowAndDefersTheRest() (gas: 28453214)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1073349)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1040274)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463642)\n[PASS] test_sweepInsideCapPeriodKeepsCachedDaysOnTheirCurve() (gas: 9967877)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10472182)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283414)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 13070319)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 60.01ms (140.94ms CPU time)\n\nRan 14 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1562608)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443849)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600373)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 27932471)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24106201)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10398069)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10607677)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 920379)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14661873)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10889696)\n[PASS] test_pendingBoostCommitmentCanBeReplacedBeforeExecution() (gas: 783484)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 480160)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6045233)\n[PASS] test_sweptBoostIsMinableAndEarlierWindowsAreUnchanged() (gas: 418506291)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 139.79ms (98.30ms CPU time)\n\nRan 17 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12656, ~: 11877)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5901728, ~: 5911444)\nLogs:\n  Bound result 9000000000000086401\n  Bound result 9000000000000002693\n  Bound result 607801\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2094891)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40213925)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1119490)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2249648)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3484896)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1606674)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2682767)\n[PASS] test_hookDepositsNeverRevertForMaintenanceAndConserveFunds() (gas: 33304358)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1155594)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1749305)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1483607)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1313909)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1542275)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2541043)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2626270)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 139.77ms (217.20ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 935063159)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 802.81ms (802.65ms CPU time)\n\nRan 6 test suites in 803.80ms (1.21s CPU time): 59 tests passed, 0 failed, 0 skipped (59 total tests)\n","passed":true},{"durationMs":54,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.prepareCap(address,address,uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":168,\"abi/HamsterBoost.json\":1232,\"abi/HamsterVault.json\":2406,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":31,\"foundry.toml\":18,\"remappings.txt\":1,\"src/HamsterBoost.sol\":689,\"src/HamsterMining.sol\":206,\"src/HamsterStaking.sol\":415,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":335,\"test/BoostExhaustion.t.sol\":110,\"test/Forwarding.t.sol\":120,\"test/Mining.t.sol\":460,\"test/Staking.t.sol\":373,\"test/V2Integration.t.sol\":125,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":7459,"exitCode":0,"name":"slither","output":"[high/medium] weak-prng at src/HamsterBoost.sol:406: HamsterBoost.emissionCurve(uint256) (src/HamsterBoost.sol#406-412) uses a weak PRNG: \"MINING_ALLOCATION - remaining + VaultMath.mulDiv(band,loss % HALF_LOSS,HALF_LOSS) (src/HamsterBoost.sol#411)\"\n[high/medium] weak-prng at src/HamsterStaking.sol:113: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#113-152) uses a weak PRNG: \"s.dustScaled += emission % s.weight (src/HamsterStaking.sol#145)\"\n[high/medium] weak-prng at src/HamsterStaking.sol:172: HamsterStaking._credit(HamsterStaking.Account,uint256,uint256) (src/HamsterStaking.sol#172-178) uses a weak PRNG: \"a.remainder = fractional % RAY (src/HamsterStaking.sol#176)\"\n[high/medium] incorrect-exp at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) has bitwise-xor operator ^ instead of the exponentiation operator **: \n[high/medium] reentrancy-balance at src/VaultBase.sol:73: Reentrancy in VaultBase._pull(address,address,uint256) (src/VaultBase.sol#73-78):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:74: Reentrancy in HamsterVault.pullGameRewards(address) (src/HamsterVault.sol#74-84):\n[high/medium] reentrancy-balance at src/VaultBase.sol:80: Reentrancy in VaultBase._push(address,address,uint256) (src/VaultBase.sol#80-89):\n[high/medium] reentrancy-balance at src/VaultBase.sol:80: Reentrancy in VaultBase._push(address,address,uint256) (src/VaultBase.sol#80-89):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:47: VaultMath.decay(uint256) (src/libraries/VaultMath.sol#47-55) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/HamsterStaking.sol:310: HamsterStaking._stream(uint256,bool) (src/HamsterStaking.sol#310-331) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/HamsterMining.sol:116: HamsterMining._resolveGame(address) (src/HamsterMining.sol#116-134) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:113: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#113-152) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:211: HamsterStaking._newLot(address,uint256,bool) (src/HamsterStaking.sol#211-222) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:291: HamsterStaking.withdrawVested(address,uint256[]) (src/HamsterStaking.sol#291-308) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:310: HamsterStaking._stream(uint256,bool) (src/HamsterStaking.sol#310-331) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:244: HamsterStaking._removeLot(address,uint256,uint256) (src/HamsterStaking.sol#244-254) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:180: HamsterStaking._syncAccount(address,uint256) (src/HamsterStaking.sol#180-194) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:113: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#113-152) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:154: HamsterStaking._checkpoint(uint256) (src/HamsterStaking.sol#154-163) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterBoost.sol:658: HamsterBoost._rawBudget(address,address,uint256) (src/HamsterBoost.sol#658-678) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:113: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#113-152) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterBoost.sol:417: HamsterBoost._curve(HamsterBoost.State,uint256) (src/HamsterBoost.sol#417-424) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/HamsterMining.sol:148: Reentrancy in HamsterMining._prepareWindow(address,uint256) (src/HamsterMining.sol#148-159):\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:224: Reentrancy in HamsterStaking.stake(uint256) (src/HamsterStaking.sol#224-232):\n[medium/medium] reentrancy-no-eth at src/HamsterMining.sol:94: Reentrancy in HamsterMining.executeGame() (src/HamsterMining.sol#94-110):\n[medium/medium] reentrancy-no-eth at src/HamsterMining.sol:116: Reentrancy in HamsterMining._resolveGame(address) (src/HamsterMining.sol#116-134):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:74: Reentrancy in HamsterVault.pullGameRewards(address) (src/HamsterVault.sol#74-84):\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:341: Reentrancy in HamsterStaking.depositDrip(uint256) (src/HamsterStaking.sol#341-349):\n[medium/medium] reentrancy-no-eth at src/HamsterMining.sol:161: Reentrancy in HamsterMining.claimMined(address,uint256[],address) (src/HamsterMining.sol#161-196):\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:335: Reentrancy in HamsterStaking.depositRewards(uint256) (src/HamsterStaking.sol#335-339):\n[medium/medium] uninitialized-local at src/HamsterBoost.sol:322: HamsterBoost.prepare(uint256).amount (src/HamsterBoost.sol#322) is a local variable never initialized\n[medium/medium] unused-return at src/HamsterMining.sol:148: HamsterMining._prepareWindow(address,uint256) (src/HamsterMining.sol#148-159) ignores return value by boostEngine.prepareCap(address(this),game,16) (src/HamsterMining.sol#150)\n[medium/medium] unused-return at src/HamsterBoost.sol:680: HamsterBoost.windowBudget(address,address,uint256) (src/HamsterBoost.sol#680-688) ignores return value by (prepared,cachedBudget,None,None,None,None) = IMiningView(vault).windowAccounting(game,k) (src/HamsterBoost.sol#686)\n[medium/medium] unused-return at src/HamsterBoost.sol:381: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#381-402) ignores return value by g.totalEligibleLoss(0) (src/HamsterBoost.sol#392)\n[medium/medium] unused-return at src/HamsterBoost.sol:381: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#381-402) ignores return value by g.lossOf(0,address(this)) (src/HamsterBoost.sol#391)\n[medium/medium] unused-return at src/HamsterBoost.sol:381: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#381-402) ignores return value by g.cumulativeLossBefore(0) (src/HamsterBoost.sol#393)\n[medium/medium] unused-return at src/HamsterBoost.sol:381: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#381-402) ignores return value by g.cumulativeLossAfter(0) (src/HamsterBoost.sol#394)\n[medium/medium] unused-return at src/HamsterBoost.sol:381: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#381-402) ignores return value by g.balances(address(this)) (src/HamsterBoost.sol#395)\n[medium/medium] unused-return at src/HamsterMining.sol:85: HamsterMining.scheduleGame(address) (src/HamsterMining.sol#85-91) ignores return value by boostEngine.checkGame(candidate) (src/HamsterMining.sol#87)\n[medium/medium] unused-return at src/HamsterBoost.sol:381: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#381-402) ignores return value by g.nextFinalizeWindow() (src/HamsterBoost.sol#390)\n[low/medium] calls-loop at src/HamsterBoost.sol:223: HamsterBoost.validRegistration(address,uint256) (src/HamsterBoost.sol#223-233) has external calls inside a loop: owner = INFT(_states[vault].collection).ownerOf(r.tokenId) (src/HamsterBoost.sol#228-232)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: before_ < g.localOrigin || after_ < before_ || after_ - before_ != total || (k == g.first && before_ != g.localOrigin) || (k > g.first && before_ != source.cumulativeLossAfter(k - 1)) (src/HamsterBoost.sol#464-466)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: before_ < g.localOrigin || after_ < before_ || after_ - before_ != total || (k == g.first && before_ != g.localOrigin) || (k > g.first && before_ != source.cumulativeLossAfter(k - 1)) (src/HamsterBoost.sol#464-466)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: source.windowStart(k) != uint256(g.genesis) + k * 3600 || source.windowEnd(k) != uint256(g.genesis) + (k + 1) * 3600 (src/HamsterBoost.sol#457-458)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: source.nextFinalizeWindow() <= k (src/HamsterBoost.sol#455)\n[low/medium] calls-loop at src/HamsterMining.sol:161: HamsterMining.claimMined(address,uint256[],address) (src/HamsterMining.sol#161-196) has external calls inside a loop: loss = IHamsterGame(game).lossOf(k,beneficiary) (src/HamsterMining.sol#173)\n[low/medium] calls-loop at src/HamsterBoost.sol:257: HamsterBoost._startDay(address,uint256) (src/HamsterBoost.sol#257-283) has external calls inside a loop: (budget,total) = v.windowBudget(game,k) (src/HamsterBoost.sol#279)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: after_ = source.cumulativeLossAfter(k) (src/HamsterBoost.sol#461)\n[low/medium] calls-loop at src/HamsterBoost.sol:536: HamsterBoost._advanceCap(address,address,uint256) (src/HamsterBoost.sol#536-557) has external calls inside a loop: g = IMiningView(vault).games(game) (src/HamsterBoost.sol#537)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: before_ = source.cumulativeLossBefore(k) (src/HamsterBoost.sol#460)\n[low/medium] calls-loop at src/HamsterMining.sol:136: HamsterMining.windowBudget(address,uint256) (src/HamsterMining.sol#136-138) has external calls inside a loop: boostEngine.windowBudget(address(this),game,k) (src/HamsterMining.sol#137)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: before_ = source.cumulativeLossBefore(k) (src/HamsterBoost.sol#460)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: after_ = source.cumulativeLossAfter(k) (src/HamsterBoost.sol#461)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: total = source.totalEligibleLoss(k) (src/HamsterBoost.sol#462)\n[low/medium] calls-loop at src/HamsterMining.sol:148: HamsterMining._prepareWindow(address,uint256) (src/HamsterMining.sol#148-159) has external calls inside a loop: boostEngine.prepareCap(address(this),game,16) (src/HamsterMining.sol#150)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: before_ = source.cumulativeLossBefore(k) (src/HamsterBoost.sol#460)\n[low/medium] calls-loop at src/HamsterBoost.sol:507: HamsterBoost._nextCapDay(address,IMiningView.GameData,HamsterBoost.State,HamsterBoost.CapState) (src/HamsterBoost.sol#507-534) has external calls inside a loop: d.lossStart = IHamsterGame(game).cumulativeLossBefore(first) (src/HamsterBoost.sol#516)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: total = source.totalEligibleLoss(k) (src/HamsterBoost.sol#462)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: source.nextFinalizeWindow() <= k (src/HamsterBoost.sol#455)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: source.nextFinalizeWindow() <= k (src/HamsterBoost.sol#455)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: after_ = source.cumulativeLossAfter(k) (src/HamsterBoost.sol#461)\n[low/medium] calls-loop at src/HamsterBoost.sol:507: HamsterBoost._nextCapDay(address,IMiningView.GameData,HamsterBoost.State,HamsterBoost.CapState) (src/HamsterBoost.sol#507-534) has external calls inside a loop: d.lossStart = IHamsterGame(game).cumulativeLossBefore(first) (src/HamsterBoost.sol#516)\n[low/medium] calls-loop at src/HamsterBoost.sol:507: HamsterBoost._nextCapDay(address,IMiningView.GameData,HamsterBoost.State,HamsterBoost.CapState) (src/HamsterBoost.sol#507-534) has external calls inside a loop: d.lossStart = IHamsterGame(game).cumulativeLossBefore(first) (src/HamsterBoost.sol#516)\n[low/medium] calls-loop at src/HamsterBoost.sol:446: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#446-468) has external calls inside a loop: sou","passed":true},{"durationMs":530,"exitCode":0,"name":"aderyn","output":"[high] incorrect-caret-operator at src/libraries/VaultMath.sol:33: Incorrect use of caret operator\n[high] reentrancy-state-change at src/HamsterBoost.sol:173: Reentrancy: State change after external call (10 places)\n[high] storage-array-memory-edit at src/HamsterBoost.sol:427: Storage Array Edited with Memory\n[high] unsafe-casting at src/HamsterMining.sol:64: Unsafe Casting of integers\n[low] costly-loop at src/HamsterBoost.sol:266: Costly operations inside loop (10 places)\n[low] large-numeric-literal at src/HamsterBoost.sol:42: Large Numeric Literal (15 places)\n[low] literal-instead-of-constant at src/HamsterBoost.sol:173: Literal Instead of Constant (20 places)\n[low] local-variable-shadowing at src/HamsterBoost.sol:85: Local Variable Shadows State Variable\n[low] require-revert-in-loop at src/HamsterBoost.sol:266: Loop Contains `require`/`revert` (15 places)\n[low] state-change-without-event at src/HamsterStaking.sol:196: State Change Without Event (7 places)\n[low] state-no-address-check at src/HamsterMining.sol:88: Address State Variable Set Without Checks\n[low] unchecked-return at src/HamsterBoost.sol:390: Unchecked Return (10 places)\n[low] uninitialized-local-variable at src/HamsterBoost.sol:306: Uninitialized Local Variable (9 places)\n[low] unsafe-erc20-operation at src/VaultBase.sol:84: Unsafe ERC20 Operation (3 places)\n[low] unused-error at src/HamsterMining.sol:52: Unused Error (2 places)","passed":true},{"durationMs":9532,"exitCode":0,"name":"proof b64053382718","output":"Compiling 32 files with Solc 0.8.26\nSolc 0.8.26 finished in 8.97s\nCompiler run successful with warnings:\nWarning (2072): Unused local variable.\n   --> test/imd-proof-e289e1e5/Proof_b64053382718.t.sol:133:12:\n    |\n133 |         (, uint256 pos, uint256 lossStart, uint256 raw, uint256 allowance) = boost.capDays(address(vault), address(next), day);\n    |            ^^^^^^^^^^^\n\nWarning (2072): Unused local variable.\n   --> test/imd-proof-e289e1e5/Proof_b64053382718.t.sol:133:25:\n    |\n133 |         (, uint256 pos, uint256 lossStart, uint256 raw, uint256 allowance) = boost.capDays(address(vault), address(next), day);\n    |                         ^^^^^^^^^^^^^^^^^\n\n\nRan 1 test for test/imd-proof-e289e1e5/Proof_b64053382718.t.sol:SweepCapTest\n[PASS] test_sweepAfterCachedCappedDayOverpaysTheDay() (gas: 5711690)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.71ms (2.04ms CPU time)\n\nRan 1 test suite in 3.55ms (2.71ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"73269e39124baf1c0d57b92465b0b9d3ec59fb75455ffc1e75b8f3d233b23932","verifiedTreeHash":"b138972bddbfb754e9320f5befafc96aa6c280cb","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":40320,"exitCode":0,"name":"build","output":"Compiling 40 files with Solc 0.8.26\nSolc 0.8.26 finished in 40.23s\nCompiler run successful!\n","passed":true},{"durationMs":4106,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 6 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_bindAcceptsDonatedExcessButNotShortfallOrWrongSupply() (gas: 1686755)\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 777223)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1432911)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 467450)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 586483)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2046449)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 25.36ms (8.40ms CPU time)\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2280290)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10222449)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2622162)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 169.98ms (6.28ms CPU time)\n\nRan 17 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12407, ~: 11717)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5902123, ~: 5911585)\nLogs:\n  Bound result 9000000000000003601\n  Bound result 9000000000000002732\n  Bound result 607801\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2094783)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40213817)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1119382)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2249540)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3484788)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1606519)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2682659)\n[PASS] test_hookDepositsNeverRevertForMaintenanceAndConserveFunds() (gas: 33304250)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1155540)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1749197)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1483499)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1313801)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1542167)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2540888)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2626162)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 198.75ms (411.37ms CPU time)\n\nRan 1 test for test/SwitchInvariants.t.sol:SwitchInvariantTest\n[PASS] invariant_switchesKeepCapBucketAndCustody() (runs: 24, calls: 720, reverts: 8)\n\n╭---------------+------------+-------+---------+----------╮\n| Contract      | Selector   | Calls | Reverts | Discards |\n+=========================================================+\n| SwitchHandler | addLoss    | 145   | 0       | 0        |\n|---------------+------------+-------+---------+----------|\n| SwitchHandler | advance    | 139   | 0       | 0        |\n|---------------+------------+-------+---------+----------|\n| SwitchHandler | claim      | 137   | 3       | 0        |\n|---------------+------------+-------+---------+----------|\n| SwitchHandler | resolve    | 152   | 5       | 0        |\n|---------------+------------+-------+---------+----------|\n| SwitchHandler | switchGame | 147   | 0       | 0        |\n╰---------------+------------+-------+---------+----------╯\n\nLogs:\n  Bound result 6\n  Bound result 255\n  Bound result 12\n  Bound result 399999999999999875001\n  Bound result 13\n  Bound result 5374\n  Bound result 23\n  Bound result 21136\n  Bound result 131819\n  Bound result 60090\n  Bound result 90\n  Bound result 7\n  Bound result 16532\n  Bound result 6\n  Bound result 399999999999999875001\n  Bound result 25237\n  Bound result 12\n  Bound result 47905422254244716691\n  Bound result 25\n  Bound result 4557\n  Bound result 83\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 684.53ms (682.56ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 934882549)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 897.19ms (897.02ms CPU time)\n\nRan 14 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1562191)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443788)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600214)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 27911720)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24103570)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10396564)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10606008)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 920230)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14659728)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10888059)\n[PASS] test_pendingBoostCommitmentCanBeReplacedBeforeExecution() (gas: 783276)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 480044)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6044292)\n[PASS] test_sweptBoostIsMinableAndEarlierWindowsAreUnchanged() (gas: 418505859)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 2.31s (127.78ms CPU time)\n\nRan 17 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2682122, ~: 2682840)\nLogs:\n  Bound result 151477097971440698442\n  Bound result 5699943125\n  Bound result 65\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 53974, ~: 54239)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158138)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 116139)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 18102135)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 834192)\n[PASS] test_halvingCrossingAndDust() (gas: 2456616)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1288211)\n[PASS] test_miningStateFallsBackToOutgoingGameWhileSuccessorUnresolved() (gas: 1341005)\n[PASS] test_newGameCapCrossingHalvingKeepsCurveConsistentAndChainsToNextGame() (gas: 13916175)\n[PASS] test_newGameDailyCapScalesEveryWindowAndDefersTheRest() (gas: 28348496)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1073092)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1040068)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463642)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10465851)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283414)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 13050378)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 2.31s (194.98ms CPU time)\n\nRan 21 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_cappedDayPaysAtMostTwoMillionAtAnyCurvePosition(uint256,uint256) (runs: 128, μ: 10550078, ~: 10604263)\nLogs:\n  Bound result 20816241293499250613239\n  Bound result 40000000000000000000000\n  Bound result 36467768161535901799226\n\n[PASS] testFuzz_newGameDailyCapNeverExceeded(uint256) (runs: 256, μ: 58601372, ~: 58601371)\nLogs:\n  Bound result 606229341614285849619\n  Bound result 289364784334154231099\n  Bound result 1614022681790328078928\n  Bound result 295474693888846357010\n  Bound result 985791182612032857766\n  Bound result 1765003006645796664200\n  Bound result 924150009064714710843\n  Bound result 1820731785046583415554\n  Bound result 1754157634859631327291\n  Bound result 1625517111525915604561\n  Bound result 907545917342495682074\n  Bound result 530369513669488335354\n  Bound result 1446836154978057602516\n  Bound result 1720298786034988202689\n  Bound result 1011196375563943700177\n  Bound result 1594716144759289706643\n  Bound result 559864297470290918843\n  Bound result 1772622536878250845279\n  Bound result 553811018609618110631\n  Bound result 1952929898162880054275\n  Bound result 1243644907327842597917\n  Bound result 397087663839341208963\n  Bound result 379376994964755292320\n  Bound result 1814903649117288699708\n  Bound result 934981023439700656050\n  Bound result 1807173537364984283640\n  Bound result 1881862596040028511776\n  Bound result 1041744931338836614016\n  Bound result 1333675024325449506536\n  Bound result 72736457090401466039\n  Bound result 127489571364616184310\n  Bound result 200643230776218813442\n  Bound result 587818461201640425796\n  Bound result 1869623643662101882662\n  Bound result 84474612689212912883\n  Bound result 1180421757530468965951\n  Bound result 28950605214712077796\n  Bound result 418565175856398762053\n  Bound result 1938604130930885606708\n  Bound result 1727791019498949613037\n  Bound result 789706669431706177564\n  Bound result 1031934000057659242689\n  Bound result 1627325928882151302375\n  Bound result 1746626925746291244669\n  Bound result 278295637313641428535\n  Bound result 1389006435044605409244\n\n[PASS] testFuzz_windowBudgetsTelescopeAcrossHalvings(uint256,uint8) (runs: 512, μ: 5927464, ~: 4767943)\nLogs:\n  Bound result 6\n  Bound result 23528422546114465784328\n  Bound result 4610386867576413166930\n  Bound result 19513645813417960075427\n  Bound result 14197107833991231482194\n  Bound result 22584300691385306214813\n  Bound result 8717266915235708101971\n  Bound result 28141823173935934588945\n  Bound result 28306705815157439320156\n  Bound result 3633752928985523534197\n  Bound result 11862406327361411630267\n  Bound result 24212983093128468432149\n  Bound result 28297183423715231273951\n\n[PASS] test_bindRejectsWrongSupplyBalanceImdAndEoa() (gas: 1268889)\n[PASS] test_boostActivationDeadlineBoundary() (gas: 723017)\n[PASS] test_boostEntryPointsInertBeforeActivation() (gas: 323288)\n[PASS] test_boostRejectsNonMainnetAndNonErc721() (gas: 299696)\n[PASS] test_constructorRejectsZeroTeamOrBoost() (gas: 6857)\n[PASS] test_directBoostCallsCannotTouchVaultNamespace() (gas: 220806)\n[PASS] test_everyTeamPowerRejectsOtherCallers() (gas: 883484)\n[PASS] test_forwardingFailurePaths() (gas: 898211)\n[PASS] test_forwardingFollowsSecondSwitchWithoutHookChange() (gas: 4358724)\n[PASS] test_gameScheduleBoundaries() (gas: 2341095)\n[PASS] test_grantLotCannotBeUnstakedOrWithdrawnEarly() (gas: 1749243)\n[PASS] test_lossAboveTotalOrOverclaimedWindowReverts() (gas: 1397393)\n[PASS] test_prepareCapBoundsAndUnknownOrUnresolvedGames() (gas: 11390422)\n[PASS] test_rateAndHalvingEdges() (gas: 147238)\n[PASS] test_rewardFailurePaths() (gas: 774034)\n[PASS] test_stakingFailurePaths() (gas: 2064954)\n[PASS] test_unknownGameAndWindowBeyondFinalization() (gas: 1046470)\n[PASS] test_zeroLossClaimConsumesSlotWithoutGrant() (gas: 1138519)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 2.44s (2.85s CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantTest\n[PASS]\nVaultInvariantTest invariants:\n[PASS] invariant_balancesWeightAndPrincipal\n[PASS] invariant_custodyEqualsLiabilities\n[PASS] invariant_miningBucketAndWindowAccounting\n[PASS] invariant_rewardsNeverExceedDeposits\n VaultInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | addLoss          | 136   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | advance          | 148   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | checkpoint       | 147   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claim            | 124   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claimRewards     | 154   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositDrip      | 120   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositRewards   | 150   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | flush            | 135   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | forward          | 122   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullGame         | 138   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | requestUnstake   | 134   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | stake            | 130   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawUnstaked | 149   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawVested   | 133   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 22746\n  Bound result 94\n  Bound result 8453\n  Bound result 11398\n  Bound result 86400\n  Bound result 12341\n  Bound result 94\n  Bound result 112425\n  Bound result 200\n  Bound result 10000000000000000000000\n  Bound result 4859440462881695197520\n  Bound result 10\n  Bound result 7200\n  Bound result 128\n  Bound result 676\n  Bound result 57\n  Bound result 300\n  Bound result 8\n  Bound result 400\n  Bound result 486\n  Bound result 12\n  Bound result 100000000000000000000\n  Bound result 127\n  Bound result 18197\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.02s (4.01s CPU time)\n\nRan 9 test suites in 4.02s (13.05s CPU time): 81 tests passed, 0 failed, 0 skipped (81 total tests)\n","passed":true},{"durationMs":49,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.prepareCap(address,address,uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":168,\"abi/HamsterBoost.json\":1198,\"abi/HamsterVault.json\":2406,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":30,\"foundry.toml\":18,\"remappings.txt\":1,\"src/HamsterBoost.sol\":668,\"src/HamsterMining.sol\":206,\"src/HamsterStaking.sol\":415,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":335,\"test/BoostExhaustion.t.sol\":110,\"test/EdgeCases.t.sol\":552,\"test/Forwarding.t.sol\":120,\"test/Mining.t.sol\":392,\"test/Staking.t.sol\":373,\"test/SwitchInvariants.t.sol\":133,\"test/V2Integration.t.sol\":125,\"test/VaultInvariants.t.sol\":321,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"823ee0cd9c1b8e066c7b26e6da5e19cf55cc3db1739bd148e462e96aaa29d5d3","verifiedTreeHash":"f14c5ba271c4a63cd12df8dfeec5d5b034980353","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":30224,"exitCode":0,"name":"build","output":"Compiling 37 files with Solc 0.8.26\nSolc 0.8.26 finished in 30.13s\nCompiler run successful!\n","passed":true},{"durationMs":998,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2280290)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10222449)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2622162)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 6.75ms (7.15ms CPU time)\n\nRan 6 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_bindAcceptsDonatedExcessButNotShortfallOrWrongSupply() (gas: 1686755)\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 777223)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1432911)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 467450)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 586483)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2046449)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 15.36ms (3.57ms CPU time)\n\nRan 14 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1562191)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443788)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600214)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 27911720)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24103570)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10396564)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10606008)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 920230)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14659728)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10888059)\n[PASS] test_pendingBoostCommitmentCanBeReplacedBeforeExecution() (gas: 783276)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 480044)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6044292)\n[PASS] test_sweptBoostIsMinableAndEarlierWindowsAreUnchanged() (gas: 418505859)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 53.39ms (80.82ms CPU time)\n\nRan 17 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2682122, ~: 2682840)\nLogs:\n  Bound result 151477097971440698442\n  Bound result 5699943125\n  Bound result 65\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 53966, ~: 54239)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158138)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 116139)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 18102135)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 834192)\n[PASS] test_halvingCrossingAndDust() (gas: 2456616)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1288211)\n[PASS] test_miningStateFallsBackToOutgoingGameWhileSuccessorUnresolved() (gas: 1341005)\n[PASS] test_newGameCapCrossingHalvingKeepsCurveConsistentAndChainsToNextGame() (gas: 13916175)\n[PASS] test_newGameDailyCapScalesEveryWindowAndDefersTheRest() (gas: 28348496)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1073092)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1040068)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463642)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10465851)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283414)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 13050378)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 131.52ms (148.27ms CPU time)\n\nRan 17 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12358, ~: 11759)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5901797, ~: 5911653)\nLogs:\n  Bound result 9000002248461796427\n  Bound result 9000000000004078294\n  Bound result 259201\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2094783)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40213817)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1119382)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2249540)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3484788)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1606519)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2682659)\n[PASS] test_hookDepositsNeverRevertForMaintenanceAndConserveFunds() (gas: 33304250)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1155540)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1749197)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1483499)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1313801)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1542167)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2540888)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2626162)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 141.51ms (188.71ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 934882549)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 903.64ms (903.47ms CPU time)\n\nRan 6 test suites in 904.56ms (1.25s CPU time): 58 tests passed, 0 failed, 0 skipped (58 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.prepareCap(address,address,uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":168,\"abi/HamsterBoost.json\":1198,\"abi/HamsterVault.json\":2406,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":30,\"foundry.toml\":18,\"remappings.txt\":1,\"src/HamsterBoost.sol\":668,\"src/HamsterMining.sol\":206,\"src/HamsterStaking.sol\":415,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":335,\"test/BoostExhaustion.t.sol\":110,\"test/Forwarding.t.sol\":120,\"test/Mining.t.sol\":392,\"test/Staking.t.sol\":373,\"test/V2Integration.t.sol\":125,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":7470,"exitCode":0,"name":"slither","output":"[high/medium] weak-prng at src/HamsterStaking.sol:113: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#113-152) uses a weak PRNG: \"s.dustScaled += emission % s.weight (src/HamsterStaking.sol#145)\"\n[high/medium] weak-prng at src/HamsterBoost.sol:397: HamsterBoost.emissionCurve(uint256) (src/HamsterBoost.sol#397-403) uses a weak PRNG: \"MINING_ALLOCATION - remaining + VaultMath.mulDiv(band,loss % HALF_LOSS,HALF_LOSS) (src/HamsterBoost.sol#402)\"\n[high/medium] weak-prng at src/HamsterStaking.sol:172: HamsterStaking._credit(HamsterStaking.Account,uint256,uint256) (src/HamsterStaking.sol#172-178) uses a weak PRNG: \"a.remainder = fractional % RAY (src/HamsterStaking.sol#176)\"\n[high/medium] incorrect-exp at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) has bitwise-xor operator ^ instead of the exponentiation operator **: \n[high/medium] reentrancy-balance at src/VaultBase.sol:73: Reentrancy in VaultBase._pull(address,address,uint256) (src/VaultBase.sol#73-78):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:74: Reentrancy in HamsterVault.pullGameRewards(address) (src/HamsterVault.sol#74-84):\n[high/medium] reentrancy-balance at src/VaultBase.sol:80: Reentrancy in VaultBase._push(address,address,uint256) (src/VaultBase.sol#80-89):\n[high/medium] reentrancy-balance at src/VaultBase.sol:80: Reentrancy in VaultBase._push(address,address,uint256) (src/VaultBase.sol#80-89):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:47: VaultMath.decay(uint256) (src/libraries/VaultMath.sol#47-55) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/HamsterStaking.sol:310: HamsterStaking._stream(uint256,bool) (src/HamsterStaking.sol#310-331) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/HamsterBoost.sol:408: HamsterBoost._curve(HamsterBoost.State,uint256) (src/HamsterBoost.sol#408-415) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterMining.sol:116: HamsterMining._resolveGame(address) (src/HamsterMining.sol#116-134) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:113: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#113-152) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:211: HamsterStaking._newLot(address,uint256,bool) (src/HamsterStaking.sol#211-222) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterBoost.sol:640: HamsterBoost._rawBudget(address,address,uint256) (src/HamsterBoost.sol#640-657) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:291: HamsterStaking.withdrawVested(address,uint256[]) (src/HamsterStaking.sol#291-308) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:310: HamsterStaking._stream(uint256,bool) (src/HamsterStaking.sol#310-331) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:244: HamsterStaking._removeLot(address,uint256,uint256) (src/HamsterStaking.sol#244-254) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:180: HamsterStaking._syncAccount(address,uint256) (src/HamsterStaking.sol#180-194) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:113: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#113-152) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:154: HamsterStaking._checkpoint(uint256) (src/HamsterStaking.sol#154-163) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:113: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#113-152) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/HamsterMining.sol:148: Reentrancy in HamsterMining._prepareWindow(address,uint256) (src/HamsterMining.sol#148-159):\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:224: Reentrancy in HamsterStaking.stake(uint256) (src/HamsterStaking.sol#224-232):\n[medium/medium] reentrancy-no-eth at src/HamsterMining.sol:94: Reentrancy in HamsterMining.executeGame() (src/HamsterMining.sol#94-110):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:74: Reentrancy in HamsterVault.pullGameRewards(address) (src/HamsterVault.sol#74-84):\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:341: Reentrancy in HamsterStaking.depositDrip(uint256) (src/HamsterStaking.sol#341-349):\n[medium/medium] reentrancy-no-eth at src/HamsterMining.sol:161: Reentrancy in HamsterMining.claimMined(address,uint256[],address) (src/HamsterMining.sol#161-196):\n[medium/medium] reentrancy-no-eth at src/HamsterMining.sol:116: Reentrancy in HamsterMining._resolveGame(address) (src/HamsterMining.sol#116-134):\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:335: Reentrancy in HamsterStaking.depositRewards(uint256) (src/HamsterStaking.sol#335-339):\n[medium/medium] uninitialized-local at src/HamsterBoost.sol:313: HamsterBoost.prepare(uint256).amount (src/HamsterBoost.sol#313) is a local variable never initialized\n[medium/medium] unused-return at src/HamsterMining.sol:148: HamsterMining._prepareWindow(address,uint256) (src/HamsterMining.sol#148-159) ignores return value by boostEngine.prepareCap(address(this),game,16) (src/HamsterMining.sol#150)\n[medium/medium] unused-return at src/HamsterBoost.sol:372: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#372-393) ignores return value by g.totalEligibleLoss(0) (src/HamsterBoost.sol#383)\n[medium/medium] unused-return at src/HamsterBoost.sol:659: HamsterBoost.windowBudget(address,address,uint256) (src/HamsterBoost.sol#659-667) ignores return value by (prepared,cachedBudget,None,None,None,None) = IMiningView(vault).windowAccounting(game,k) (src/HamsterBoost.sol#665)\n[medium/medium] unused-return at src/HamsterBoost.sol:372: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#372-393) ignores return value by g.lossOf(0,address(this)) (src/HamsterBoost.sol#382)\n[medium/medium] unused-return at src/HamsterBoost.sol:372: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#372-393) ignores return value by g.cumulativeLossAfter(0) (src/HamsterBoost.sol#385)\n[medium/medium] unused-return at src/HamsterBoost.sol:372: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#372-393) ignores return value by g.balances(address(this)) (src/HamsterBoost.sol#386)\n[medium/medium] unused-return at src/HamsterBoost.sol:372: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#372-393) ignores return value by g.nextFinalizeWindow() (src/HamsterBoost.sol#381)\n[medium/medium] unused-return at src/HamsterMining.sol:85: HamsterMining.scheduleGame(address) (src/HamsterMining.sol#85-91) ignores return value by boostEngine.checkGame(candidate) (src/HamsterMining.sol#87)\n[medium/medium] unused-return at src/HamsterBoost.sol:372: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#372-393) ignores return value by g.cumulativeLossBefore(0) (src/HamsterBoost.sol#384)\n[low/medium] calls-loop at src/HamsterBoost.sol:498: HamsterBoost._nextCapDay(address,IMiningView.GameData,HamsterBoost.State,HamsterBoost.CapState) (src/HamsterBoost.sol#498-525) has external calls inside a loop: d.lossStart = IHamsterGame(game).cumulativeLossBefore(first) (src/HamsterBoost.sol#507)\n[low/medium] calls-loop at src/HamsterBoost.sol:248: HamsterBoost._startDay(address,uint256) (src/HamsterBoost.sol#248-274) has external calls inside a loop: (budget,total) = v.windowBudget(game,k) (src/HamsterBoost.sol#270)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: source.windowStart(k) != uint256(g.genesis) + k * 3600 || source.windowEnd(k) != uint256(g.genesis) + (k + 1) * 3600 (src/HamsterBoost.sol#448-449)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: before_ = source.cumulativeLossBefore(k) (src/HamsterBoost.sol#451)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: after_ = source.cumulativeLossAfter(k) (src/HamsterBoost.sol#452)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: source.nextFinalizeWindow() <= k (src/HamsterBoost.sol#446)\n[low/medium] calls-loop at src/HamsterBoost.sol:498: HamsterBoost._nextCapDay(address,IMiningView.GameData,HamsterBoost.State,HamsterBoost.CapState) (src/HamsterBoost.sol#498-525) has external calls inside a loop: d.lossStart = IHamsterGame(game).cumulativeLossBefore(first) (src/HamsterBoost.sol#507)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: source.nextFinalizeWindow() <= k (src/HamsterBoost.sol#446)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: source.windowStart(k) != uint256(g.genesis) + k * 3600 || source.windowEnd(k) != uint256(g.genesis) + (k + 1) * 3600 (src/HamsterBoost.sol#448-449)\n[low/medium] calls-loop at src/HamsterMining.sol:161: HamsterMining.claimMined(address,uint256[],address) (src/HamsterMining.sol#161-196) has external calls inside a loop: loss = IHamsterGame(game).lossOf(k,beneficiary) (src/HamsterMining.sol#173)\n[low/medium] calls-loop at src/HamsterBoost.sol:527: HamsterBoost._advanceCap(address,address,uint256) (src/HamsterBoost.sol#527-547) has external calls inside a loop: nextFinalize = IHamsterGame(game).nextFinalizeWindow() (src/HamsterBoost.sol#537)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: before_ = source.cumulativeLossBefore(k) (src/HamsterBoost.sol#451)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: before_ < g.localOrigin || after_ < before_ || after_ - before_ != total || (k == g.first && before_ != g.localOrigin) || (k > g.first && before_ != source.cumulativeLossAfter(k - 1)) (src/HamsterBoost.sol#455-457)\n[low/medium] calls-loop at src/HamsterMining.sol:136: HamsterMining.windowBudget(address,uint256) (src/HamsterMining.sol#136-138) has external calls inside a loop: boostEngine.windowBudget(address(this),game,k) (src/HamsterMining.sol#137)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: total = source.totalEligibleLoss(k) (src/HamsterBoost.sol#453)\n[low/medium] calls-loop at src/HamsterMining.sol:148: HamsterMining._prepareWindow(address,uint256) (src/HamsterMining.sol#148-159) has external calls inside a loop: boostEngine.prepareCap(address(this),game,16) (src/HamsterMining.sol#150)\n[low/medium] calls-loop at src/HamsterBoost.sol:278: HamsterBoost.prepare(uint256) (src/HamsterBoost.sol#278-336) has external calls inside a loop: loss = ILoss(w.game).lossOf(w.k,r_scope_2.account) (src/HamsterBoost.sol#319)\n[low/medium] calls-loop at src/HamsterBoost.sol:498: HamsterBoost._nextCapDay(address,IMiningView.GameData,HamsterBoost.State,HamsterBoost.CapState) (src/HamsterBoost.sol#498-525) has external calls inside a loop: d.lossStart = IHamsterGame(game).cumulativeLossBefore(first) (src/HamsterBoost.sol#507)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: total = source.totalEligibleLoss(k) (src/HamsterBoost.sol#453)\n[low/medium] calls-loop at src/HamsterBoost.sol:527: HamsterBoost._advanceCap(address,address,uint256) (src/HamsterBoost.sol#527-547) has external calls inside a loop: g = IMiningView(vault).games(game) (src/HamsterBoost.sol#528)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: total = source.totalEligibleLoss(k) (src/HamsterBoost.sol#453)\n[low/medium] calls-loop at src/HamsterBoost.sol:498: HamsterBoost._nextCapDay(address,IMiningView.GameData,HamsterBoost.State,HamsterBoost.CapState) (src/HamsterBoost.sol#498-525) has external calls inside a loop: d.lossStart = IHamsterGame(game).cumulativeLossBefore(first) (src/HamsterBoost.sol#507)\n[low/medium] calls-loop at src/HamsterBoost.sol:437: HamsterBoost._lossRange(address,IMiningView.GameData,uint256) (src/HamsterBoost.sol#437-459) has external calls inside a loop: before_ < g.localOrigin || after_ < before_ || after_ - before_ != total || (k == g.first && before_ != g.localOrigin) || (k > g.first && before_ != source.cumulativeLossAfter(k - 1)) (src/HamsterBoost.sol#455-457)\n[low/medium] calls-loop at src/HamsterBoost.sol:248: HamsterBoost._sta","passed":true},{"durationMs":509,"exitCode":0,"name":"aderyn","output":"[high] incorrect-caret-operator at src/libraries/VaultMath.sol:33: Incorrect use of caret operator\n[high] reentrancy-state-change at src/HamsterBoost.sol:164: Reentrancy: State change after external call (10 places)\n[high] storage-array-memory-edit at src/HamsterBoost.sol:418: Storage Array Edited with Memory\n[high] unsafe-casting at src/HamsterMining.sol:64: Unsafe Casting of integers\n[low] costly-loop at src/HamsterBoost.sol:257: Costly operations inside loop (10 places)\n[low] large-numeric-literal at src/HamsterBoost.sol:42: Large Numeric Literal (15 places)\n[low] literal-instead-of-constant at src/HamsterBoost.sol:164: Literal Instead of Constant (20 places)\n[low] local-variable-shadowing at src/HamsterBoost.sol:85: Local Variable Shadows State Variable\n[low] require-revert-in-loop at src/HamsterBoost.sol:257: Loop Contains `require`/`revert` (15 places)\n[low] state-change-without-event at src/HamsterStaking.sol:196: State Change Without Event (7 places)\n[low] state-no-address-check at src/HamsterMining.sol:88: Address State Variable Set Without Checks\n[low] unchecked-return at src/HamsterBoost.sol:381: Unchecked Return (10 places)\n[low] uninitialized-local-variable at src/HamsterBoost.sol:297: Uninitialized Local Variable (9 places)\n[low] unsafe-erc20-operation at src/VaultBase.sol:84: Unsafe ERC20 Operation (3 places)\n[low] unused-error at src/HamsterMining.sol:52: Unused Error (2 places)","passed":true},{"durationMs":9160,"exitCode":0,"name":"proof 7bd5e48cabb1","output":"Compiling 32 files with Solc 0.8.26\nSolc 0.8.26 finished in 8.60s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-bf68d90d/Proof_7bd5e48cabb1.t.sol:BindGriefTest\n[PASS] test_oneWeiDonationBeforeBindBricksVaultForever() (gas: 140955)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 736.98µs (91.02µs CPU time)\n\nRan 1 test suite in 2.63ms (736.98µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":6231,"exitCode":0,"name":"proof 35331b7b18df","output":"2026-10-20T17:30:50.674924Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-CUgzzP/repo/test/imd-proof-bf68d90d/Proof_7bd5e48cabb1.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.74s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-5a2455a9/Proof_35331b7b18df.t.sol:SweepLockTest\n[PASS] test_sweptBoostBucketMustBeMinable() (gas: 1649375)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 928.04µs (792.74µs CPU time)\n\nRan 1 test suite in 3.09ms (928.04µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"88b19dd1c10a00b6f71ab142172bb3bd52000f82f5b86a1337d2ce15c25b7e67","verifiedTreeHash":"42da5f8fa0409ba3495e2d1350f0fadf527675f3","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":28187,"exitCode":0,"name":"build","output":"Compiling 37 files with Solc 0.8.26\nSolc 0.8.26 finished in 28.11s\nCompiler run successful!\n","passed":true},{"durationMs":915,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2236488)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10208973)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2596095)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 4.47ms (5.29ms CPU time)\n\nRan 12 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1406338)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443880)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600203)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 29417421)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24128297)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10460707)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10665513)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 915719)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14762775)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10917378)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 448587)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6067231)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 26.96ms (60.20ms CPU time)\n\nRan 5 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 770142)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1425890)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 460354)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 579364)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2039336)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 145.10ms (3.35ms CPU time)\n\nRan 16 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12679, ~: 11875)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5894090, ~: 5904395)\nLogs:\n  Bound result 9000000000000000025\n  Bound result 1000000000000000000\n  Bound result 604806\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2087831)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40210691)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1112401)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2242712)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3477799)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1579878)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2675651)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1153356)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1742203)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1476515)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1306878)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1535258)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2396029)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2619236)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 149.86ms (190.77ms CPU time)\n\nRan 15 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2575443, ~: 2576170)\nLogs:\n  Bound result 870205425\n  Bound result 775942298165855532517\n  Bound result 13281\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 23913, ~: 24173)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158106)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 57774)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 15993848)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 800261)\n[PASS] test_halvingCrossingAndDust() (gas: 2368787)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1225357)\n[PASS] test_newGameDailyCapScalesEveryWindowAndExpires() (gas: 22063789)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1029358)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1013938)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463653)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10259299)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283355)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 12334834)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 149.95ms (118.16ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 928308593)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 822.39ms (822.19ms CPU time)\n\nRan 6 test suites in 823.27ms (1.30s CPU time): 52 tests passed, 0 failed, 0 skipped (52 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":163,\"abi/HamsterBoost.json\":980,\"abi/HamsterVault.json\":2425,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":30,\"foundry.toml\":18,\"remappings.txt\":1,\"src/HamsterBoost.sol\":446,\"src/HamsterMining.sol\":207,\"src/HamsterStaking.sol\":409,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":275,\"test/BoostExhaustion.t.sol\":110,\"test/Forwarding.t.sol\":86,\"test/Mining.t.sol\":321,\"test/Staking.t.sol\":343,\"test/V2Integration.t.sol\":125,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":6234,"exitCode":0,"name":"slither","output":"[high/medium] weak-prng at src/HamsterStaking.sol:170: HamsterStaking._credit(HamsterStaking.Account,uint256,uint256) (src/HamsterStaking.sol#170-176) uses a weak PRNG: \"a.remainder = fractional % RAY (src/HamsterStaking.sol#174)\"\n[high/medium] weak-prng at src/HamsterStaking.sol:111: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#111-150) uses a weak PRNG: \"s.dustScaled += emission % s.weight (src/HamsterStaking.sol#143)\"\n[high/medium] incorrect-exp at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) has bitwise-xor operator ^ instead of the exponentiation operator **: \n[high/medium] reentrancy-balance at src/VaultBase.sol:73: Reentrancy in VaultBase._pull(address,address,uint256) (src/VaultBase.sol#73-78):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:74: Reentrancy in HamsterVault.pullGameRewards(address) (src/HamsterVault.sol#74-84):\n[high/medium] reentrancy-balance at src/VaultBase.sol:80: Reentrancy in VaultBase._push(address,address,uint256) (src/VaultBase.sol#80-89):\n[high/medium] reentrancy-balance at src/VaultBase.sol:80: Reentrancy in VaultBase._push(address,address,uint256) (src/VaultBase.sol#80-89):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:47: VaultMath.decay(uint256) (src/libraries/VaultMath.sol#47-55) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/HamsterStaking.sol:306: HamsterStaking._stream(uint256,bool) (src/HamsterStaking.sol#306-327) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/libraries/VaultMath.sol:9: VaultMath.mulDiv(uint256,uint256,uint256) (src/libraries/VaultMath.sol#9-42) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:242: HamsterStaking._removeLot(address,uint256,uint256) (src/HamsterStaking.sol#242-252) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:111: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#111-150) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:111: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#111-150) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:111: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#111-150) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:306: HamsterStaking._stream(uint256,bool) (src/HamsterStaking.sol#306-327) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:288: HamsterStaking.withdrawVested(address,uint256[]) (src/HamsterStaking.sol#288-304) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:152: HamsterStaking._checkpoint(uint256) (src/HamsterStaking.sol#152-161) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:178: HamsterStaking._syncAccount(address,uint256) (src/HamsterStaking.sol#178-192) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterMining.sol:120: HamsterMining._resolveGame(address) (src/HamsterMining.sol#120-135) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterStaking.sol:209: HamsterStaking._newLot(address,uint256,bool) (src/HamsterStaking.sol#209-220) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:329: Reentrancy in HamsterStaking.depositRewards(uint256) (src/HamsterStaking.sol#329-333):\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:335: Reentrancy in HamsterStaking.depositDrip(uint256) (src/HamsterStaking.sol#335-343):\n[medium/medium] reentrancy-no-eth at src/HamsterStaking.sol:222: Reentrancy in HamsterStaking.stake(uint256) (src/HamsterStaking.sol#222-230):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:74: Reentrancy in HamsterVault.pullGameRewards(address) (src/HamsterVault.sol#74-84):\n[medium/medium] uninitialized-local at src/HamsterBoost.sol:439: HamsterBoost.windowBudget(address,address,uint256).sum (src/HamsterBoost.sol#439) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HamsterBoost.sol:286: HamsterBoost.prepare(uint256).amount (src/HamsterBoost.sol#286) is a local variable never initialized\n[medium/medium] unused-return at src/HamsterBoost.sol:345: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#345-366) ignores return value by g.balances(address(this)) (src/HamsterBoost.sol#359)\n[medium/medium] unused-return at src/HamsterBoost.sol:345: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#345-366) ignores return value by g.totalEligibleLoss(0) (src/HamsterBoost.sol#356)\n[medium/medium] unused-return at src/HamsterBoost.sol:345: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#345-366) ignores return value by g.cumulativeLossAfter(0) (src/HamsterBoost.sol#358)\n[medium/medium] unused-return at src/HamsterBoost.sol:345: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#345-366) ignores return value by g.nextFinalizeWindow() (src/HamsterBoost.sol#354)\n[medium/medium] unused-return at src/HamsterBoost.sol:345: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#345-366) ignores return value by g.lossOf(0,address(this)) (src/HamsterBoost.sol#355)\n[medium/medium] unused-return at src/HamsterMining.sol:89: HamsterMining.scheduleGame(address) (src/HamsterMining.sol#89-95) ignores return value by boostEngine.checkGame(candidate) (src/HamsterMining.sol#91)\n[medium/medium] unused-return at src/HamsterBoost.sol:424: HamsterBoost.windowBudget(address,address,uint256) (src/HamsterBoost.sol#424-445) ignores return value by (prepared,cachedBudget,None,None,None,None) = IMiningView(vault).windowAccounting(game,k) (src/HamsterBoost.sol#430)\n[medium/medium] unused-return at src/HamsterBoost.sol:345: HamsterBoost.checkGame(address) (src/HamsterBoost.sol#345-366) ignores return value by g.cumulativeLossBefore(0) (src/HamsterBoost.sol#357)\n[low/medium] calls-loop at src/HamsterMining.sol:162: HamsterMining.claimMined(address,uint256[],address) (src/HamsterMining.sol#162-197) has external calls inside a loop: loss = IHamsterGame(game).lossOf(k,beneficiary) (src/HamsterMining.sol#174)\n[low/medium] calls-loop at src/HamsterMining.sol:137: HamsterMining.windowBudget(address,uint256) (src/HamsterMining.sol#137-139) has external calls inside a loop: boostEngine.windowBudget(address(this),game,k) (src/HamsterMining.sol#138)\n[low/medium] calls-loop at src/HamsterBoost.sol:188: HamsterBoost.validRegistration(address,uint256) (src/HamsterBoost.sol#188-198) has external calls inside a loop: owner = INFT(_states[vault].collection).ownerOf(r.tokenId) (src/HamsterBoost.sol#193-197)\n[low/medium] calls-loop at src/HamsterBoost.sol:251: HamsterBoost.prepare(uint256) (src/HamsterBoost.sol#251-309) has external calls inside a loop: loss = ILoss(w.game).lossOf(w.k,r_scope_2.account) (src/HamsterBoost.sol#292)\n[low/medium] calls-loop at src/HamsterBoost.sol:222: HamsterBoost._startDay(address,uint256) (src/HamsterBoost.sol#222-247) has external calls inside a loop: game = v.gameList(idx) (src/HamsterBoost.sol#232)\n[low/medium] calls-loop at src/HamsterBoost.sol:222: HamsterBoost._startDay(address,uint256) (src/HamsterBoost.sol#222-247) has external calls inside a loop: (budget,total) = v.windowBudget(game,k) (src/HamsterBoost.sol#243)\n[low/medium] calls-loop at src/HamsterBoost.sol:222: HamsterBoost._startDay(address,uint256) (src/HamsterBoost.sol#222-247) has external calls inside a loop: g = v.games(game) (src/HamsterBoost.sol#233)\n[low/medium] reentrancy-benign at src/HamsterVault.sol:48: Reentrancy in HamsterVault.claimBoost(uint256,uint256) (src/HamsterVault.sol#48-53):\n[low/medium] reentrancy-benign at src/HamsterStaking.sol:222: Reentrancy in HamsterStaking.stake(uint256) (src/HamsterStaking.sol#222-230):\n[low/medium] reentrancy-benign at src/HamsterVault.sol:74: Reentrancy in HamsterVault.pullGameRewards(address) (src/HamsterVault.sol#74-84):\n[low/medium] reentrancy-benign at src/HamsterStaking.sol:329: Reentrancy in HamsterStaking.depositRewards(uint256) (src/HamsterStaking.sol#329-333):\n[low/medium] reentrancy-benign at src/HamsterStaking.sol:335: Reentrancy in HamsterStaking.depositDrip(uint256) (src/HamsterStaking.sol#335-343):\n[low/medium] reentrancy-benign at src/HamsterVault.sol:55: Reentrancy in HamsterVault.sweepDormantBoost() (src/HamsterVault.sol#55-58):\n[low/medium] reentrancy-benign at src/HamsterVault.sol:86: Reentrancy in HamsterVault.forwardJackpot(uint256) (src/HamsterVault.sol#86-90):\n[low/medium] reentrancy-benign at src/HamsterVault.sol:92: Reentrancy in HamsterVault.forwardOracle(uint256) (src/HamsterVault.sol#92-96):\n[low/medium] reentrancy-events at src/HamsterVault.sol:48: Reentrancy in HamsterVault.claimBoost(uint256,uint256) (src/HamsterVault.sol#48-53):\n[low/medium] reentrancy-events at src/HamsterStaking.sol:329: Reentrancy in HamsterStaking.depositRewards(uint256) (src/HamsterStaking.sol#329-333):\n[low/medium] reentrancy-events at src/HamsterVault.sol:86: Reentrancy in HamsterVault.forwardJackpot(uint256) (src/HamsterVault.sol#86-90):\n[low/medium] reentrancy-events at src/HamsterVault.sol:110: Reentrancy in HamsterVault._flush(uint256,bool) (src/HamsterVault.sol#110-119):\n[low/medium] reentrancy-events at src/HamsterStaking.sol:268: Reentrancy in HamsterStaking.withdrawUnstaked(address,uint256[]) (src/HamsterStaking.sol#268-281):\n[low/medium] reentrancy-events at src/HamsterStaking.sol:345: Reentrancy in HamsterStaking.claimRewards(address) (src/HamsterStaking.sol#345-353):\n[low/medium] reentrancy-events at src/HamsterVault.sol:74: Reentrancy in HamsterVault.pullGameRewards(address) (src/HamsterVault.sol#74-84):\n[low/medium] reentrancy-events at src/HamsterStaking.sol:335: Reentrancy in HamsterStaking.depositDrip(uint256) (src/HamsterStaking.sol#335-343):\n[low/medium] reentrancy-events at src/HamsterVault.sol:92: Reentrancy in HamsterVault.forwardOracle(uint256) (src/HamsterVault.sol#92-96):\n[low/medium] reentrancy-events at src/HamsterStaking.sol:222: Reentrancy in HamsterStaking.stake(uint256) (src/HamsterStaking.sol#222-230):\n[low/medium] reentrancy-events at src/HamsterStaking.sol:288: Reentrancy in HamsterStaking.withdrawVested(address,uint256[]) (src/HamsterStaking.sol#288-304):\n[low/medium] timestamp at src/HamsterBoost.sol:424: HamsterBoost.windowBudget(address,address,uint256) (src/HamsterBoost.sol#424-445) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:194: HamsterStaking.checkpointAccount(address,uint256) (src/HamsterStaking.sol#194-198) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterBoost.sol:137: HamsterBoost.schedule(address) (src/HamsterBoost.sol#137-145) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterMining.sol:120: HamsterMining._resolveGame(address) (src/HamsterMining.sol#120-135) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterBoost.sol:251: HamsterBoost.prepare(uint256) (src/HamsterBoost.sol#251-309) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterMining.sol:85: HamsterMining._firstAt(HamsterMining.Game,uint256) (src/HamsterMining.sol#85-87) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:111: HamsterStaking._step(HamsterStaking.RewardState,uint256) (src/HamsterStaking.sol#111-150) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:178: HamsterStaking._syncAccount(address,uint256) (src/HamsterStaking.sol#178-192) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterMining.sol:98: HamsterMining.executeGame() (src/HamsterMining.sol#98-114) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterBoost.sol:311: HamsterBoost.claimable(address,uint256,uint256) (src/HamsterBoost.sol#311-316) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterBoost.sol:160: HamsterBoost.sweep() (src/HamsterBoost.sol#160-167) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterBoost.sol:188: HamsterBoost.validRegistration(address,uint256) (src/HamsterBoost.sol#188-198) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:209: HamsterStaking._newLot(address,uint256,bool) (src/HamsterStaking.sol#209-220) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterBoost.sol:169: HamsterBoost.register(address,uint256) (src/HamsterBoost.sol#169-186) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterMining.sol:199: HamsterMining.gameAtTime(uint256) (src/HamsterMining.sol#199-206) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:306: HamsterStaking._stream(uint256,bool) (src/HamsterStaking.sol#306-327) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:152: HamsterStaking._checkpoint(uint256) (src/HamsterStaking.sol#152-161) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:242: HamsterStaking._removeLot(address,uint256,uint256) (src/HamsterStaking.sol#242-252) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:355: HamsterStaking._preview() (src/HamsterStaking.sol#355-364) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:288: HamsterStaking.withdrawVested(address,uint256[]) (src/HamsterStaking.sol#288-304) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:366: HamsterStaking.pendingIMD(address) (src/HamsterStaking.sol#366-384) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterStaking.sol:268: HamsterStaking.withdrawUnstaked(address,uint256[]) (src/HamsterStaking.sol#268-281) uses tim","passed":true},{"durationMs":432,"exitCode":0,"name":"aderyn","output":"[high] incorrect-caret-operator at src/libraries/VaultMath.sol:33: Incorrect use of caret operator\n[high] reentrancy-state-change at src/HamsterBoost.sol:140: Reentrancy: State change after external call (10 places)\n[high] unsafe-casting at src/HamsterMining.sol:64: Unsafe Casting of integers\n[low] costly-loop at src/HamsterBoost.sol:231: Costly operations inside loop (9 places)\n[low] large-numeric-literal at src/HamsterBoost.sol:42: Large Numeric Literal (15 places)\n[low] literal-instead-of-constant at src/HamsterBoost.sol:140: Literal Instead of Constant (10 places)\n[low] local-variable-shadowing at src/HamsterBoost.sol:84: Local Variable Shadows State Variable\n[low] require-revert-in-loop at src/HamsterBoost.sol:283: Loop Contains `require`/`revert` (10 places)\n[low] state-change-without-event at src/HamsterStaking.sol:194: State Change Without Event (7 places)\n[low] state-no-address-check at src/HamsterMining.sol:92: Address State Variable Set Without Checks\n[low] unchecked-return at src/HamsterBoost.sol:354: Unchecked Return (7 places)\n[low] uninitialized-local-variable at src/HamsterBoost.sol:270: Uninitialized Local Variable (7 places)\n[low] unsafe-erc20-operation at src/VaultBase.sol:84: Unsafe ERC20 Operation (3 places)\n[low] unused-error at src/HamsterMining.sol:52: Unused Error (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a2384c73a15534f57d4ee05aabf0e5f648cc2ee6fae23929cfb5ad8e8fb2759f","verifiedTreeHash":"45cbae5878fc16714d8b126574f46733bb7aef7e","verifierVersion":"0.1.0+475a429a"},{"checks":[{"durationMs":30404,"exitCode":0,"name":"build","output":"Compiling 37 files with Solc 0.8.26\nSolc 0.8.26 finished in 30.32s\nCompiler run successful!\n","passed":true},{"durationMs":972,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 17 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2682122, ~: 2682840)\nLogs:\n  Bound result 151477097971440698442\n  Bound result 5699943125\n  Bound result 65\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 53966, ~: 54239)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158138)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 116139)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 18102135)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 834192)\n[PASS] test_halvingCrossingAndDust() (gas: 2456616)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1288211)\n[PASS] test_miningStateFallsBackToOutgoingGameWhileSuccessorUnresolved() (gas: 1341005)\n[PASS] test_newGameCapCrossingHalvingKeepsCurveConsistentAndChainsToNextGame() (gas: 13916175)\n[PASS] test_newGameDailyCapScalesEveryWindowAndDefersTheRest() (gas: 28348496)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1073092)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1040068)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463642)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10465851)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283414)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 13050378)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 84.33ms (188.84ms CPU time)\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2280290)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10222449)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2622162)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 131.04ms (8.48ms CPU time)\n\nRan 14 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1562191)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443788)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600214)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 27911720)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24103570)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10396564)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10606008)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 920230)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14659728)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10888059)\n[PASS] test_pendingBoostCommitmentCanBeReplacedBeforeExecution() (gas: 783276)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 480044)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6044292)\n[PASS] test_sweptBoostIsMinableAndEarlierWindowsAreUnchanged() (gas: 418505859)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 136.02ms (100.62ms CPU time)\n\nRan 6 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_bindAcceptsDonatedExcessButNotShortfallOrWrongSupply() (gas: 1686755)\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 777223)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1432911)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 467450)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 586483)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2046449)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 135.99ms (4.79ms CPU time)\n\nRan 17 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12358, ~: 11759)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5901797, ~: 5911653)\nLogs:\n  Bound result 9000002248461796427\n  Bound result 9000000000004078294\n  Bound result 259201\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2094783)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40213817)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1119382)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2249540)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3484788)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1606519)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2682659)\n[PASS] test_hookDepositsNeverRevertForMaintenanceAndConserveFunds() (gas: 33304250)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1155540)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1749197)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1483499)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1313801)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1542167)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2540888)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2626162)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 142.78ms (195.54ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 934882549)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 882.52ms (882.36ms CPU time)\n\nRan 6 test suites in 883.24ms (1.51s CPU time): 58 tests passed, 0 failed, 0 skipped (58 total tests)\n","passed":true},{"durationMs":48,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.prepareCap(address,address,uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":168,\"abi/HamsterBoost.json\":1198,\"abi/HamsterVault.json\":2406,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":30,\"foundry.toml\":18,\"launch.json\":17,\"remappings.txt\":1,\"src/HamsterBoost.sol\":668,\"src/HamsterMining.sol\":206,\"src/HamsterStaking.sol\":415,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":335,\"test/BoostExhaustion.t.sol\":110,\"test/Forwarding.t.sol\":120,\"test/Mining.t.sol\":392,\"test/Staking.t.sol\":373,\"test/V2Integration.t.sol\":125,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a9f3e3dc0dbcd5624930372868cc58b4a482207d5303aec5ccdfefb5c11f1969","verifiedTreeHash":"55ba95b5f8b6d94b84d9198f6f9365a5410a8f5a","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":25227,"exitCode":0,"name":"build","output":"Compiling 37 files with Solc 0.8.26\nSolc 0.8.26 finished in 25.15s\nCompiler run successful!\n","passed":true},{"durationMs":971,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2236488)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10208973)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2596095)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 3.96ms (6.18ms CPU time)\n\nRan 5 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 770142)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1425890)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 460354)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 579364)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2039336)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 6.86ms (7.06ms CPU time)\n\nRan 12 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1406338)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443880)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600203)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 29417421)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24128297)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10460707)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10665513)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 915719)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14762775)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10917378)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 448587)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6067231)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 24.99ms (69.30ms CPU time)\n\nRan 15 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2575443, ~: 2576170)\nLogs:\n  Bound result 151477097971440698442\n  Bound result 5699943125\n  Bound result 65\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 23913, ~: 24173)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158106)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 57774)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 15993848)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 800261)\n[PASS] test_halvingCrossingAndDust() (gas: 2368787)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1225357)\n[PASS] test_newGameDailyCapScalesEveryWindowAndExpires() (gas: 22063789)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1029358)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1013938)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463653)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10259299)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283355)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 12334834)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 148.14ms (107.04ms CPU time)\n\nRan 16 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12679, ~: 11875)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5894090, ~: 5904395)\nLogs:\n  Bound result 9000000125635229474\n  Bound result 9000000349528170482\n  Bound result 437791\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2087831)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40210691)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1112401)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2242712)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3477799)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1579878)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2675651)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1153356)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1742203)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1476515)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1306878)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1535258)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2396029)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2619236)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 148.18ms (188.02ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 928308593)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 888.69ms (888.55ms CPU time)\n\nRan 6 test suites in 890.25ms (1.22s CPU time): 52 tests passed, 0 failed, 0 skipped (52 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":163,\"abi/HamsterBoost.json\":980,\"abi/HamsterVault.json\":2425,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":30,\"foundry.toml\":18,\"launch.json\":17,\"remappings.txt\":1,\"src/HamsterBoost.sol\":446,\"src/HamsterMining.sol\":207,\"src/HamsterStaking.sol\":409,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":275,\"test/BoostExhaustion.t.sol\":110,\"test/Forwarding.t.sol\":86,\"test/Mining.t.sol\":321,\"test/Staking.t.sol\":343,\"test/V2Integration.t.sol\":125,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"b3847826751c8593052fd1c0687368b49371ce40da6a28ddc2455d48abbba244","verifiedTreeHash":"ab9f8b07cbcde8b28c9d0787a359bb540835ccb6","verifierVersion":"0.1.0+475a429a"},{"checks":[{"durationMs":44972,"exitCode":0,"name":"build","output":"Compiling 41 files with Solc 0.8.26\nSolc 0.8.26 finished in 44.89s\nCompiler run successful!\n","passed":true},{"durationMs":4382,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2280518)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10222585)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2622317)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 12.61ms (11.42ms CPU time)\n\nRan 17 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12528, ~: 11679)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5902421, ~: 5911680)\nLogs:\n  Bound result 9000000000000000017\n  Bound result 9000000000000002693\n  Bound result 607801\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2094891)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40213925)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1119490)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2249648)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3484896)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1606674)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2682767)\n[PASS] test_hookDepositsNeverRevertForMaintenanceAndConserveFunds() (gas: 33304358)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1155594)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1749305)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1483607)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1313909)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1542275)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2541043)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2626270)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 181.50ms (382.84ms CPU time)\n\nRan 14 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1562608)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443849)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600373)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 27932471)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24106201)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10398069)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10607677)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 920379)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14661873)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10889696)\n[PASS] test_pendingBoostCommitmentCanBeReplacedBeforeExecution() (gas: 783484)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 480160)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6045233)\n[PASS] test_sweptBoostIsMinableAndEarlierWindowsAreUnchanged() (gas: 418506291)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 181.51ms (116.95ms CPU time)\n\nRan 1 test for test/SwitchInvariants.t.sol:SwitchInvariantTest\n[PASS] invariant_switchesKeepCapBucketAndCustody() (runs: 24, calls: 720, reverts: 9)\n\n╭---------------+------------+-------+---------+----------╮\n| Contract      | Selector   | Calls | Reverts | Discards |\n+=========================================================+\n| SwitchHandler | addLoss    | 145   | 0       | 0        |\n|---------------+------------+-------+---------+----------|\n| SwitchHandler | advance    | 139   | 0       | 0        |\n|---------------+------------+-------+---------+----------|\n| SwitchHandler | claim      | 137   | 3       | 0        |\n|---------------+------------+-------+---------+----------|\n| SwitchHandler | resolve    | 152   | 6       | 0        |\n|---------------+------------+-------+---------+----------|\n| SwitchHandler | switchGame | 147   | 0       | 0        |\n╰---------------+------------+-------+---------+----------╯\n\nLogs:\n  Bound result 4\n  Bound result 255\n  Bound result 12\n  Bound result 399999999999999875001\n  Bound result 0\n  Bound result 5547\n  Bound result 7\n  Bound result 21118\n  Bound result 131819\n  Bound result 111593\n  Bound result 86\n  Bound result 8\n  Bound result 16584\n  Bound result 17\n  Bound result 399999999999999875001\n  Bound result 169213\n  Bound result 23\n  Bound result 77718465966463075098\n  Bound result 20\n  Bound result 6022\n  Bound result 76\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 793.87ms (792.30ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 935063159)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 877.95ms (877.80ms CPU time)\n\nRan 2 tests for test/SweepCapInvariants.t.sol:SweepCapInvariantTest\n[PASS] invariant_sweepInsideCapKeepsDaysAndBuckets() (runs: 32, calls: 1920, reverts: 85)\n\n╭-----------------+------------+-------+---------+----------╮\n| Contract        | Selector   | Calls | Reverts | Discards |\n+===========================================================+\n| SweepCapHandler | addLoss    | 226   | 0       | 0        |\n|-----------------+------------+-------+---------+----------|\n| SweepCapHandler | advance    | 236   | 0       | 0        |\n|-----------------+------------+-------+---------+----------|\n| SweepCapHandler | claim      | 254   | 30      | 0        |\n|-----------------+------------+-------+---------+----------|\n| SweepCapHandler | look       | 227   | 0       | 0        |\n|-----------------+------------+-------+---------+----------|\n| SweepCapHandler | prepare    | 273   | 46      | 0        |\n|-----------------+------------+-------+---------+----------|\n| SweepCapHandler | resolve    | 251   | 9       | 0        |\n|-----------------+------------+-------+---------+----------|\n| SweepCapHandler | sweep      | 242   | 0       | 0        |\n|-----------------+------------+-------+---------+----------|\n| SweepCapHandler | switchGame | 211   | 0       | 0        |\n╰-----------------+------------+-------+---------+----------╯\n\nLogs:\n  Bound result 23\n  Bound result 21077\n  Bound result 4\n  Bound result 15923\n  Bound result 16\n  Bound result 24\n  Bound result 14406897\n  Bound result 0\n  Bound result 16\n  Bound result 2\n  Bound result 25\n  Bound result 16788\n  Bound result 21600\n  Bound result 6\n  Bound result 299999999999999999997\n  Bound result 100\n  Bound result 0\n  Bound result 2\n  Bound result 199999999999999998334\n  Bound result 63\n  Bound result 3774\n  Bound result 2\n  Bound result 16\n  Bound result 13194\n  Bound result 11\n  Bound result 16\n  Bound result 43460\n  Bound result 72608\n  Bound result 114\n  Bound result 14\n  Bound result 14\n  Bound result 91055\n\n[PASS] test_sweepOpensInsideFirstCapPeriod() (gas: 38140)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.17s (1.17s CPU time)\n\nRan 18 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2682565, ~: 2683278)\nLogs:\n  Bound result 107984262110738209\n  Bound result 38528336477069297\n  Bound result 30685626785546424996\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 54029, ~: 54305)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158160)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 116271)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 18332582)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 834361)\n[PASS] test_halvingCrossingAndDust() (gas: 2457012)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1288540)\n[PASS] test_miningStateFallsBackToOutgoingGameWhileSuccessorUnresolved() (gas: 1341049)\n[PASS] test_newGameCapCrossingHalvingKeepsCurveConsistentAndChainsToNextGame() (gas: 13951820)\n[PASS] test_newGameDailyCapScalesEveryWindowAndDefersTheRest() (gas: 28453214)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1073349)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1040274)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463642)\n[PASS] test_sweepInsideCapPeriodKeepsCachedDaysOnTheirCurve() (gas: 9967877)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10472182)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283414)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 13070319)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 2.48s (323.15ms CPU time)\n\nRan 6 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_bindAcceptsDonatedExcessButNotShortfallOrWrongSupply() (gas: 1686809)\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 777331)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1433019)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 467558)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 586591)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2046557)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 2.48s (3.94ms CPU time)\n\nRan 21 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_cappedDayPaysAtMostTwoMillionAtAnyCurvePosition(uint256,uint256) (runs: 128, μ: 10572458, ~: 10620558)\nLogs:\n  Bound result 20816241293499250613239\n  Bound result 40000000000000000000000\n  Bound result 36467768161535901799226\n\n[PASS] testFuzz_newGameDailyCapNeverExceeded(uint256) (runs: 256, μ: 58857675, ~: 58857673)\nLogs:\n  Bound result 606229341614285849619\n  Bound result 289364784334154231099\n  Bound result 1614022681790328078928\n  Bound result 295474693888846357010\n  Bound result 985791182612032857766\n  Bound result 1765003006645796664200\n  Bound result 924150009064714710843\n  Bound result 1820731785046583415554\n  Bound result 1754157634859631327291\n  Bound result 1625517111525915604561\n  Bound result 907545917342495682074\n  Bound result 530369513669488335354\n  Bound result 1446836154978057602516\n  Bound result 1720298786034988202689\n  Bound result 1011196375563943700177\n  Bound result 1594716144759289706643\n  Bound result 559864297470290918843\n  Bound result 1772622536878250845279\n  Bound result 553811018609618110631\n  Bound result 1952929898162880054275\n  Bound result 1243644907327842597917\n  Bound result 397087663839341208963\n  Bound result 379376994964755292320\n  Bound result 1814903649117288699708\n  Bound result 934981023439700656050\n  Bound result 1807173537364984283640\n  Bound result 1881862596040028511776\n  Bound result 1041744931338836614016\n  Bound result 1333675024325449506536\n  Bound result 72736457090401466039\n  Bound result 127489571364616184310\n  Bound result 200643230776218813442\n  Bound result 587818461201640425796\n  Bound result 1869623643662101882662\n  Bound result 84474612689212912883\n  Bound result 1180421757530468965951\n  Bound result 28950605214712077796\n  Bound result 418565175856398762053\n  Bound result 1938604130930885606708\n  Bound result 1727791019498949613037\n  Bound result 789706669431706177564\n  Bound result 1031934000057659242689\n  Bound result 1627325928882151302375\n  Bound result 1746626925746291244669\n  Bound result 278295637313641428535\n  Bound result 1389006435044605409244\n\n[PASS] testFuzz_windowBudgetsTelescopeAcrossHalvings(uint256,uint8) (runs: 512, μ: 5863015, ~: 4768407)\nLogs:\n  Bound result 6\n  Bound result 1103605523692760675696\n  Bound result 19945255650814015327837\n  Bound result 28916965375544182050611\n  Bound result 7112598317206081172489\n  Bound result 1265303916724241546047\n  Bound result 27107519101649981201179\n  Bound result 12519758724345006799362\n  Bound result 21759579320804615028738\n  Bound result 19937176287704968305538\n  Bound result 13135596341954402756557\n  Bound result 7744773825732632079410\n  Bound result 15081052595000019141036\n\n[PASS] test_bindRejectsWrongSupplyBalanceImdAndEoa() (gas: 1268889)\n[PASS] test_boostActivationDeadlineBoundary() (gas: 723259)\n[PASS] test_boostEntryPointsInertBeforeActivation() (gas: 323452)\n[PASS] test_boostRejectsNonMainnetAndNonErc721() (gas: 299762)\n[PASS] test_constructorRejectsZeroTeamOrBoost() (gas: 6857)\n[PASS] test_directBoostCallsCannotTouchVaultNamespace() (gas: 220926)\n[PASS] test_everyTeamPowerRejectsOtherCallers() (gas: 883484)\n[PASS] test_forwardingFailurePaths() (gas: 898319)\n[PASS] test_forwardingFollowsSecondSwitchWithoutHookChange() (gas: 4358855)\n[PASS] test_gameScheduleBoundaries() (gas: 2341095)\n[PASS] test_grantLotCannotBeUnstakedOrWithdrawnEarly() (gas: 1749398)\n[PASS] test_lossAboveTotalOrOverclaimedWindowReverts() (gas: 1397633)\n[PASS] test_prepareCapBoundsAndUnknownOrUnresolvedGames() (gas: 11463552)\n[PASS] test_rateAndHalvingEdges() (gas: 147436)\n[PASS] test_rewardFailurePaths() (gas: 774142)\n[PASS] test_stakingFailurePaths() (gas: 2065062)\n[PASS] test_unknownGameAndWindowBeyondFinalization() (gas: 1046530)\n[PASS] test_zeroLossClaimConsumesSlotWithoutGrant() (gas: 1138693)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 2.48s (2.87s CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantTest\n[PASS]\nVaultInvariantTest invariants:\n[PASS] invariant_balancesWeightAndPrincipal\n[PASS] invariant_custodyEqualsLiabilities\n[PASS] invariant_miningBucketAndWindowAccounting\n[PASS] invariant_rewardsNeverExceedDeposits\n VaultInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | addLoss          | 136   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | advance          | 148   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | checkpoint       | 147   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claim            | 124   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claimRewards     | 154   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositDrip      | 120   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositRewards   | 150   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | flush            | 135   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | forward          | 122   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullGame         | 138   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | requestUnstake   | 134   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | stake            | 130   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawUnstaked | 149   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawVested   | 133   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 22524\n  Bound result 94\n  Bound result 5999999999999999999201\n  Bound result 11397\n  Bound result 86400\n  Bound result 12538\n  Bound result 94\n  Bound result 8794\n  Bound result 1899999999999999999997\n  Bound result 7500000000000000000000\n  Bound result 4859440462881695197520\n  Bound result 1999999999999999999967\n  Bound result 10000000000000000000000\n  Bound result 128\n  Bound result 2016\n  Bound result 98\n  Bound result 2100\n  Bound result 8\n  Bound result 3721019202\n  Bound result 1880\n  Bound result 90\n  Bound result 100000000000000000000\n  Bound result 127\n  Bound result 18830\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.30s (4.29s CPU time)\n\nRan 10 test suites in 4.30s (14.96s CPU time): 84 tests passed, 0 failed, 0 skipped (84 total tests)\n","passed":true},{"durationMs":45,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.prepareCap(address,address,uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":168,\"abi/HamsterBoost.json\":1232,\"abi/HamsterVault.json\":2406,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":31,\"foundry.toml\":18,\"remappings.txt\":1,\"src/HamsterBoost.sol\":689,\"src/HamsterMining.sol\":206,\"src/HamsterStaking.sol\":415,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":335,\"test/BoostExhaustion.t.sol\":110,\"test/EdgeCases.t.sol\":552,\"test/Forwarding.t.sol\":120,\"test/Mining.t.sol\":460,\"test/Staking.t.sol\":373,\"test/SweepCapInvariants.t.sol\":249,\"test/SwitchInvariants.t.sol\":133,\"test/V2Integration.t.sol\":125,\"test/VaultInvariants.t.sol\":321,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f4bab37b10c3ae2538b51cae6182e6d1a51ad10371ed2bccfe3fc85bc903066d","verifiedTreeHash":"f68c1acb1c3aa8a7c508229d5dacdc493ec04b0d","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":33946,"exitCode":0,"name":"build","output":"Compiling 37 files with Solc 0.8.26\nSolc 0.8.26 finished in 33.85s\nCompiler run successful!\n","passed":true},{"durationMs":1132,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 6 tests for test/Forwarding.t.sol:ForwardingTest\n[PASS] test_bindAcceptsDonatedExcessButNotShortfallOrWrongSupply() (gas: 1686809)\n[PASS] test_failedGameFlushRollsBackAndCannotSpendRewards() (gas: 777331)\n[PASS] test_gamePullChecksRegistrationAndReceivedAmount() (gas: 1433019)\n[PASS] test_gameReturningSuccessWithoutPullingCannotClearForwardingBalance() (gas: 467558)\n[PASS] test_partialFlushKeepsBucketsSeparate() (gas: 586591)\n[PASS] test_queuedFundsFollowCurrentGameAfterSwitch() (gas: 2046557)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 5.25ms (4.38ms CPU time)\n\nRan 3 tests for test/V2Integration.t.sol:V2IntegrationTest\n[PASS] test_actualV2CrossMarketNettingRetainsUnclaimableRemainder() (gas: 2280518)\n[PASS] test_actualV2InterfaceAcceptedWithoutMovingFundsOnSchedule() (gas: 10222585)\n[PASS] test_actualV2RetroactiveMiningRecipientBacklogAndForwarding() (gas: 2622317)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 6.37ms (6.09ms CPU time)\n\nRan 14 tests for test/Boost.t.sol:BoostTest\n[PASS] test_365DaySweepOnceAndCannotActivateAfterwards() (gas: 1562608)\n[PASS] test_activatedCollectionPreventsSweep() (gas: 443849)\n[PASS] test_activationTimelockAuthorizationMainnetAndOneShot() (gas: 600373)\n[PASS] test_globalBoostCapSpansOldAndNewGameOnSwitchDay() (gas: 27932471)\n[PASS] test_globalDailyCapOrderIndependenceAndBoundedPreparation() (gas: 24106201)\n[PASS] test_multipleNFTsIncreaseDailyCapCapacity() (gas: 10398069)\n[PASS] test_multipleNFTsShareFiftyPercentAndNoCompanionNamespaceBypass() (gas: 10607677)\n[PASS] test_newOwnerRegistersAgainWithFreshWarmup() (gas: 920379)\n[PASS] test_nftCapAppliesAcrossAllWindowsAndResetsNextUtcDay() (gas: 14661873)\n[PASS] test_ownerChangeVoidsBeforeAndAfterPreparation() (gas: 10889696)\n[PASS] test_pendingBoostCommitmentCanBeReplacedBeforeExecution() (gas: 783484)\n[PASS] test_pendingUnexecutedActivationCannotBlock365DaySweep() (gas: 480160)\n[PASS] test_registerRequiresOwnershipAnd24HourLead() (gas: 6045233)\n[PASS] test_sweptBoostIsMinableAndEarlierWindowsAreUnchanged() (gas: 418506291)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 71.56ms (117.99ms CPU time)\n\nRan 18 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_claimOrderIndependentAndConserved(uint96,uint96,uint96) (runs: 256, μ: 2682565, ~: 2683278)\nLogs:\n  Bound result 151477097971440698442\n  Bound result 5699943125\n  Bound result 65\n\n[PASS] testFuzz_curveMonotoneAdditiveAndBounded(uint128,uint128) (runs: 256, μ: 54034, ~: 54305)\n[PASS] test_bindChecksAndImmutableTeam() (gas: 1158160)\n[PASS] test_curveKnownBoundariesAndRate() (gas: 116271)\n[PASS] test_emptyChainConstructorsAndRuntimePolicy() (gas: 18332582)\n[PASS] test_finalizationBoundsDuplicatesAndInvalidBeneficiary() (gas: 834361)\n[PASS] test_halvingCrossingAndDust() (gas: 2457012)\n[PASS] test_miningBucketExhaustsWithoutOvercommitment() (gas: 1288540)\n[PASS] test_miningStateFallsBackToOutgoingGameWhileSuccessorUnresolved() (gas: 1341049)\n[PASS] test_newGameCapCrossingHalvingKeepsCurveConsistentAndChainsToNextGame() (gas: 13951820)\n[PASS] test_newGameDailyCapScalesEveryWindowAndDefersTheRest() (gas: 28453214)\n[PASS] test_poolNettingRemainderRemainsReservedForever() (gas: 1073349)\n[PASS] test_retroactivePermissionlessClaimCreatesOnlyBeneficiaryGrant() (gas: 1040274)\n[PASS] test_rewardsCanWaitBeforeBindingButHamsterActionsCannot() (gas: 463642)\n[PASS] test_sweepInsideCapPeriodKeepsCachedDaysOnTheirCurve() (gas: 9967877)\n[PASS] test_switchOverlappingBacklogNeverResetsOrDoubleCountsLoss() (gas: 10472182)\n[PASS] test_switchTimelockHasNoEarlyEffectAndChecksInterface() (gas: 1283414)\n[PASS] test_switchTwiceChainsFinalRanges() (gas: 13070319)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 76.17ms (228.20ms CPU time)\n\nRan 17 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_dripDecayBoundedAndMonotone(uint32) (runs: 256, μ: 12656, ~: 11877)\n[PASS] testFuzz_unattendedActivationsMatchFrequentCheckpoints(uint64,uint64,uint32) (runs: 256, μ: 5901728, ~: 5911444)\nLogs:\n  Bound result 9000000125635229474\n  Bound result 9000000349528170482\n  Bound result 437791\n\n[PASS] test_automaticWarmupAndTimeProRata() (gas: 2094891)\n[PASS] test_boundedCheckpointRecoversAfterManyActivations() (gas: 40213925)\n[PASS] test_cancelWarmupAndPartialUnstake() (gas: 1119490)\n[PASS] test_claimFrequencyCannotFarmRounding() (gas: 2249648)\n[PASS] test_continuousDripAnchoredAndCheckpointFrequencyIndependent() (gas: 3484896)\n[PASS] test_earlyVestingWithdrawalReducesOnlyItsWarmup() (gas: 1606674)\n[PASS] test_gameBacklogIsThirtyDayStreamAndOldGameRemainsPullable() (gas: 2682767)\n[PASS] test_hookDepositsNeverRevertForMaintenanceAndConserveFunds() (gas: 33304358)\n[PASS] test_optionalERC20ReturnValuesSupported() (gas: 1155594)\n[PASS] test_outgoingRewardReentrancyCannotConsumeCredit() (gas: 1749305)\n[PASS] test_topupsDoNotRestartExistingSevenDayStream() (gas: 1483607)\n[PASS] test_transferFailuresAndReentrancyRollBackAccounting() (gas: 1313909)\n[PASS] test_unstakeStopsWeightNowAndReleasesAfter24Hours() (gas: 1542275)\n[PASS] test_vestingUnvestedAndVestedUnwithdrawnAreWeight() (gas: 2541043)\n[PASS] test_zeroWeightPausesClockAndNoBacklogWindfall() (gas: 2626270)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 167.26ms (298.31ms CPU time)\n\nRan 1 test for test/BoostExhaustion.t.sol:BoostExhaustionTest\n[PASS] test_full50MBucketFinalPartialDayAndNoClaimOrderRace() (gas: 935063159)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 990.89ms (990.71ms CPU time)\n\nRan 6 test suites in 993.14ms (1.32s CPU time): 59 tests passed, 0 failed, 0 skipped (59 total tests)\n","passed":true},{"durationMs":79,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterBoost.claim(uint256,uint256)\",\"HamsterBoost.execute()\",\"HamsterBoost.invalidate(uint256)\",\"HamsterBoost.prepare(uint256)\",\"HamsterBoost.prepareCap(address,address,uint256)\",\"HamsterBoost.register(address,uint256)\",\"HamsterBoost.schedule(address)\",\"HamsterBoost.sweep()\",\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.checkpoint(uint256)\",\"HamsterVault.checkpointAccount(address,uint256)\",\"HamsterVault.claimBoost(uint256,uint256)\",\"HamsterVault.claimMined(address,uint256[],address)\",\"HamsterVault.claimRewards(address)\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.executeBoost()\",\"HamsterVault.executeGame()\",\"HamsterVault.flushJackpot(uint256)\",\"HamsterVault.flushOracle(uint256)\",\"HamsterVault.forwardJackpot(uint256)\",\"HamsterVault.forwardOracle(uint256)\",\"HamsterVault.invalidateRegistration(uint256)\",\"HamsterVault.prepareBoostDay(uint256)\",\"HamsterVault.pullGameRewards(address)\",\"HamsterVault.registerNFT(uint256)\",\"HamsterVault.requestUnstake(uint256,uint256)\",\"HamsterVault.resolveGame(address)\",\"HamsterVault.scheduleGame(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepDormantBoost()\",\"HamsterVault.withdrawUnstaked(address,uint256[])\",\"HamsterVault.withdrawVested(address,uint256[])\"],\"files\":{\".gitignore\":3,\"README.md\":168,\"abi/HamsterBoost.json\":1232,\"abi/HamsterVault.json\":2406,\"docs/OpenZeppelin-LICENSE\":22,\"docs/REVIEW.md\":34,\"docs/THIRD_PARTY.md\":7,\"docs/VALIDATION.md\":31,\"foundry.toml\":18,\"launch.json\":17,\"remappings.txt\":1,\"src/HamsterBoost.sol\":689,\"src/HamsterMining.sol\":206,\"src/HamsterStaking.sol\":415,\"src/HamsterVault.sol\":130,\"src/VaultBase.sol\":97,\"src/interfaces/Interfaces.sol\":34,\"src/libraries/VaultMath.sol\":60,\"test/Boost.t.sol\":335,\"test/BoostExhaustion.t.sol\":110,\"test/Forwarding.t.sol\":120,\"test/Mining.t.sol\":460,\"test/Staking.t.sol\":373,\"test/V2Integration.t.sol\":125,\"test/helpers/Mocks.sol\":194,\"test/helpers/VaultTestBase.sol\":97,\"test/reference/HamsterHour.sol\":1030,\"test/reference/OracleAttestation.sol\":81,\"test/reference/interfaces/IIntake.sol\":35},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"fb92d01b164e0ebdde6c25045a6960ff1c85099590a162ed5e274908b0c2e3f7","verifiedTreeHash":"df5604c646a91abf2ac2fe5d41db18ef669ae3c1","verifierVersion":"0.1.0+a26ee00d"}]}