{"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":"6eaa6406-fb99-4eb8-a1bf-12cc3ba9c2a9","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"bbdfbe754d11530509114ff9bb0897240bb29811c865b702cc55ccaddff68fa6","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":"60e52ac6f137d30c047e8ffb165880fdb315228c8007b662ffdabf0b9e56e725","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":"7d652dfb6ffa340c0a4d0d6a81ac3e41d919720a12b0a6f754f4aec0b7e238d4","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":"0b124000fd6d5cbf6e538cad5c04250306ac945216bebcad06f3ac2acb22dc13","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":"4ef4005911feaa933057338688a48a3516343e0f74349646d6f495fae9f2fd2d","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":"4f9f93683c5530c5d4aab46591ab584088f403ebdd34c0afd7360836352bd516","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":"63b51e740b8daa8f01cb7d20e0f18857fb496de25df77eac1b553bc0c258d244","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"4162b2ad6e418b9825fc3d152d6a1664710a0b5405bfef5054abc2c1d45c3eab","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":"Launch 1 of 2 for $HAMSTER: deploy HamsterVault on Ethereum mainnet (evm_contracts, no token yet). Launch 2 will be a Uniswap v4 hook token, HAMSTER (1B supply, 18 decimals), whose remainderTo is this vault, delivering exactly 800,000,000 HAMSTER. Its hook will deposit IMD into this vault.\n\nGame: HamsterHour v2 0xe33ab493249a2df0d8c4a71ac54f46d1f7778367 (source and ABI: github.com/identity-md-launches/launch-2260-build-hamster-hour-v2). IMD 0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7. Use the game's lossOf(k,a), totalEligibleLoss[k], cumulativeLossBefore[k], cumulativeLossAfter[k] and its staking-recipient pull balance (withdraw). TEAM = launch wallet. No owner, pause, upgrade or withdrawal path. TEAM powers are only the two one-shot actions below.\n\nTOKEN BIND: bindToken(token), TEAM only, once. Requires token.totalSupply() == 1e27 and token.balanceOf(vault) == 8e26. Buckets: mining 750M, boost 50M.\n\nMINING (pull, retroactive from the first v2 window): rate 20,000 HAMSTER per 1 IMD of eligible loss, halving every 18,750 IMD of cumulative eligible loss (total converges to 750M). Window budget = F(cumulativeLossAfter[k]) - F(cumulativeLossBefore[k]) across crossed halvings; account share = budget * lossOf(k,a) / totalEligibleLoss[k], rounded down, dust to a tracked bucket. claimMined(windows[], beneficiary) is permissionless but always pays the beneficiary, once per (k, account), bounded batches. Committed emissions never exceed the bucket; no claim order changes anyone's amount.\n\nVESTING: each claim creates a 7-day linear, non-transferable grant. Unvested and vested-unwithdrawn balances count as stake weight automatically.\n\nSTAKING (rewards in IMD only, never from the HAMSTER buckets):\n- stake/unstake liquid HAMSTER. Weight earns only after a 24h warm-up. requestUnstake stops weight immediately; tokens release after 24h. A checkpoint before every weight change.\n- Reward sources: (a) the game's 5% staker share, pulled from the game; (b) depositRewards(amount) for the hook's 0.5% swap share, streamed over 7 days; (c) depositDrip(amount) for 80% of the anti-snipe tax, a perpetual time-based drip of 1% of the remaining pot per day (continuous, not pokeable).\n- The game's pre-vault backlog streams over 30 days.\n- Strictly pro-rata by eligible weight over time. If total weight is zero, rewards wait for the next interval: never an instant payout to the first staker. Reserve buckets never count as weight.\n\nSQUEEK BOOST (50M, dormant): activateBoost(collection), TEAM once, 12h timelock, only for the canonical Squeek NFT collection on Ethereum mainnet. Holders register tokenId to their wallet without transferring it; a registration earns only for windows starting 24h+ after registering and is void if ownerOf changes. +50% of base mined, max 10,000 HAMSTER per NFT per UTC day (by window start) and 100,000 per day globally, allocated deterministically, same 7-day vesting. If not activated within 365 days of deploy, anyone can move the 50M into the mining bucket once (the curve is unchanged).\n\nVIEWS for the site: claimable mined per window, grants, stake, warm-up, unstake queue, pending IMD, drip and stream state, buckets left, current rate and halving.\n\nDeliver: Foundry, tests (retroactive claims, halving crossing within a window, claim-order independence, conservation of both buckets, vesting-as-stake, warm-up, unstake timing, zero-staker periods, drip math, no first-staker windfall, bind checks, boost registration and transfer voiding, daily caps, 365-day sweep), ABI, README, deploy params with real addresses only. After deploy, TEAM will call scheduleStakingRecipient(vault) on the game.","parentJobId":null,"planHash":"d76bf7c17b59868bfa937ebe3e643e1b0ab725c945d0d180f25c8d830dbdfeb3","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"6eaa6406-fb99-4eb8-a1bf-12cc3ba9c2a9","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2302-launch-1-2-hamster"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52286","feedbackHash":"8085126657f54273a833c650a31988ede6c95e408bb90dc49c97d32bbc15c66f","nodeKey":"audit_economics","submissionHash":"bbdfbe754d11530509114ff9bb0897240bb29811c865b702cc55ccaddff68fa6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51081","feedbackHash":"c116b11c54b178a4968311af90ab1098fa1175e2c6755e9166a9e39c9e60d657","nodeKey":"audit_flow","submissionHash":"60e52ac6f137d30c047e8ffb165880fdb315228c8007b662ffdabf0b9e56e725","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51537","feedbackHash":"aa7456990bc5cbca254aaf7eb64164713c5941e836df64f49e9021fa80f7cd40","nodeKey":"audit_judge","submissionHash":"aac45b244899ca3e7350b34dd726377e1a3df3e22f79ee9be05eddc557264c16","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52272","feedbackHash":"cb2d1532a389aa1e974dbeb0bf92fcf4d754ddd3fc90052c792ceedd6ce9d630","nodeKey":"audit_judge","submissionHash":"7d652dfb6ffa340c0a4d0d6a81ac3e41d919720a12b0a6f754f4aec0b7e238d4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50952","feedbackHash":"3d26c171af53097ee007c12709265974f2d1a35d203482b48a856560e6ef7f07","nodeKey":"audit_math","submissionHash":"0b124000fd6d5cbf6e538cad5c04250306ac945216bebcad06f3ac2acb22dc13","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52666","feedbackHash":"05d3173d62654d691c35fdd175a692894a15b9fdb700d061517cd47088ba3d4a","nodeKey":"audit_permissions","submissionHash":"4ef4005911feaa933057338688a48a3516343e0f74349646d6f495fae9f2fd2d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52231","feedbackHash":"d9e111096e808597290a127bd85e7356476f2110d6e725a90c7d9c98c2c4b6ee","nodeKey":"build_contract_project","submissionHash":"4f9f93683c5530c5d4aab46591ab584088f403ebdd34c0afd7360836352bd516","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52734","feedbackHash":"ab20d7c9e22234705c27d48dbd9c9c5f406068c6ecbeba9e5cca246161597052","nodeKey":"build_contract_project","submissionHash":"6016085ebb3a5dae59eeb67eaea4a7e80226f2387e8c5f00c01de0dfe570343c","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51008","feedbackHash":"1821ba82c100ecd0cc5a3998c2a0f19a9c003030873d38c014b747ebe2ddd8d2","nodeKey":"manifest","submissionHash":"e79e4bc12a2d8050c7f19b9f5321841cb4050e3fc3320eef5eec3e9a6aa976fa","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51234","feedbackHash":"4b450aed4803ed4652349b2eefefa1ef318d8b924ce30532c57a1bf76bd775ee","nodeKey":"manifest","submissionHash":"63b51e740b8daa8f01cb7d20e0f18857fb496de25df77eac1b553bc0c258d244","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"53911","feedbackHash":"4943a7febeaed3f2dd1d0414df6873a2482cb4f518c634fe7d55edc5bf783b19","nodeKey":"write_foundry_tests","submissionHash":"4162b2ad6e418b9825fc3d152d6a1664710a0b5405bfef5054abc2c1d45c3eab","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51483","feedbackHash":"d4467be29a184d2ef936773700e3390750f5177e483ceea000ec04bc54810892","nodeKey":"write_foundry_tests","submissionHash":"bf07c42cbae571f4e1fb62fef4968fabbf124e47f4b7cf62257a74c3c9b756b8","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"9f2f8511ef545e4414c6d2d3f273b183361d974b60daa3ba570d6580decbb238","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ce6eaff570c608ab","findings":[{"citation":"resolved","description":"Boundary: an external return value (token.balanceOf(vault)) that anyone can move is compared with strict equality, and the vault has no path that moves HAMSTER out before the bind. Launch 2 delivers the 800M remainder to this vault in the token's deployment transaction and the token is immediately tradable through its Uniswap v4 pool; TEAM binds in a later transaction. Any holder (a buyer with dust, or a bot that sees bindToken in the mempool) sends 1 wei of HAMSTER to the vault. From then on balanceOf(vault) == 8e26 + 1, bindToken reverts WrongRemainder on every attempt, and nothing in the contract (no owner, no sweep, no withdrawal) can restore the exact balance. Result: token never bound, claimMined/stake/withdrawVested revert NotBound forever, the 800,000,000 HAMSTER are locked and the whole launch is dead; the only remedy is a new vault, but the token's remainderTo already delivered to this one. The same strict equality on totalSupply() == 1e27 (line 222) bricks the bind if the hook token burns any unit (for example if the non-drip 20% of the anti-snipe tax is burned) before TEAM binds. Minimal fix that keeps the brief's guarantee: revert only when balanceOf(vault) < EXPECTED_REMAINDER (and totalSupply() < EXPECTED_SUPPLY if burns are possible); buckets stay 750M/50M and any surplus is simply untracked, as donations after the bind already are.","line":223,"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\";\n\n/// @dev Minimal 18-decimal ERC20 standing in for the launch-2 HAMSTER token.\ncontract ProofToken {\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        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @notice Finding: `bindToken` requires `balanceOf(vault) == 8e26` exactly. Any holder can send\n/// 1 wei of HAMSTER to the vault between launch 2 (which delivers the 800M remainder) and TEAM's\n/// bind transaction. The vault has no withdrawal path, so the bind can never succeed afterwards and\n/// the 800M HAMSTER plus every future claim/stake is bricked forever.\n///\n/// Fails on the current code (bind reverts WrongRemainder); passes once the remainder check\n/// tolerates a surplus (e.g. `balanceOf(vault) < EXPECTED_REMAINDER` reverts, surplus is untracked).\ncontract BindGriefProof is Test {\n    address team = makeAddr(\"team\");\n    address game = makeAddr(\"game\"); // never called by bindToken\n    address attacker = makeAddr(\"attacker\");\n\n    function test_oneWeiDonationBeforeBindBricksTheVault() public {\n        HamsterVault vault = new HamsterVault(team, game);\n        ProofToken hamster = new ProofToken();\n        // Launch 2: 1B supply, remainderTo = vault delivers exactly 800M.\n        hamster.mint(address(vault), 8e26);\n        hamster.mint(attacker, 2e26); // the rest is in circulation (pool / buyers)\n\n        // Attacker donates 1 wei to the vault before TEAM binds (or front-runs the bind tx).\n        vm.prank(attacker);\n        hamster.transfer(address(vault), 1);\n\n        assertEq(hamster.totalSupply(), 1e27);\n        assertGe(hamster.balanceOf(address(vault)), 8e26);\n\n        // Expected: TEAM can still bind; the vault holds at least the delivered 800M.\n        // Actual on current code: WrongRemainder, forever (no path moves the extra wei out).\n        vm.prank(team);\n        vault.bindToken(address(hamster));\n\n        assertEq(address(vault.token()), address(hamster));\n        assertEq(vault.miningBucket(), 750_000_000e18);\n        assertEq(vault.boostBucket(), 50_000_000e18);\n    }\n}","reproduction":"State: vault deployed; HAMSTER totalSupply 1e27, balanceOf(vault) 8e26. Attacker (holding >= 1 wei HAMSTER) calls hamster.transfer(vault, 1). TEAM calls vault.bindToken(hamster). Expected: bind succeeds (the vault holds at least the delivered 800M), miningBucket 750M, boostBucket 50M. Actual: revert WrongRemainder on this and every later attempt; no function can reduce the vault's balance, so the vault is permanently unbound. Proof test test/scratch/BindGrief.t.sol fails with WrongRemainder() on the current code.","severity":"high","snippet":"        if (IERC20(token_).balanceOf(address(this)) != EXPECTED_REMAINDER) revert WrongRemainder();","title":"bindToken requires balanceOf(vault) == 8e26 exactly: a 1-wei HAMSTER donation before the bind bricks the vault forever"},{"citation":"resolved","description":"Numerical gap (boundary x invariant): inside one claimMined call boostCommitted is only increased after the loop (line 274), so each window's _allocateBoost computes bucketLeft = boostBucket - boostCommitted from the value before the call and may allocate up to the whole remaining bucket per window. The sum of a batch can exceed the bucket, which the belt-and-braces check at line 275 turns into a revert of the whole claim instead of capping the later windows. The same windows claimed one per call succeed (the first takes the rest of the bucket, the next gets 0 boost). Impact: when the 50M boost bucket is nearly exhausted, batched claims for boosted accounts revert until the caller splits the batch; base mining is not lost, only the UX guarantee that any finalized set of windows can be claimed in one bounded batch. Fix: track boost committed inside the loop (add got to boostCommitted in _allocateBoost, or pass a running total) so later windows see the reduced room.","line":683,"path":"src/HamsterVault.sol","reproduction":"State: boost active, alice has one registered Squeek (registered > 24h before the windows), boostBucket - boostCommitted = 15,000e18. Windows k and k+24 (different UTC days) each record 1 IMD of eligible loss for alice with total 1 IMD, so base = 20,000 HAMSTER and boost wanted = 10,000 per window. claimMined([k, k+24], alice): each window sees bucketLeft 15,000 and takes 10,000; boostCommitted becomes bucket + 5,000 -> revert BucketExceeded. claimMined([k]) then claimMined([k+24]) succeed with boosts 10,000 and 5,000 (boostCommitted == boostBucket). Reproduced in test/scratch/BoostBatch.t.sol (sets boostCommitted via vm.store on slot 4).","severity":"low","snippet":"        uint256 bucketLeft = boostBucket - boostCommitted;","title":"Multi-window claimMined reverts BucketExceeded near boost-bucket exhaustion because _allocateBoost reads a stale boostCommitted for every window in the batch"},{"citation":"resolved","description":"Boundary (minimum input): depositRewards is permissionless and accepts amount >= 1 wei; _addToStream merges the leftover and resets end = now + 7 days on every call. A caller that deposits 1 wei once a day makes the remaining amount decay geometrically (factor 6/7 per day) instead of finishing after 7 days: with 7,000 IMD deposited, 2,379 IMD are still undistributed at the original end (day 7) and 808 IMD at day 14. Nothing is lost and the attack costs only gas plus 1 wei per call, but the brief's 'streamed over 7 days' is not enforceable, and stakers who leave after the nominal 7 days forfeit the tail to later stakers. Note that a hook calling depositRewards per swap has the same effect by itself. Fix options preserving the design: do not move end on deposits below a minimum (keep the current rate and add to remaining with a pro-rata end), or extend end only by the share of new amount over the merged total.","line":453,"path":"src/HamsterVault.sol","reproduction":"State: alice staked 100 HAMSTER and active; depositRewards(7,000e18) at T (end T+7d). Attacker calls depositRewards(1) at T+1d, T+2d, ... T+14d. Expected (7-day stream): stream.remaining == 0 at T+7d. Actual: stream.remaining == 2,379e18 at T+7d, 808e18 at T+14d, stream.end always 7 days after the last 1-wei deposit. Reproduced in test/scratch/Misc.t.sol:test_oneWeiDepositsStretchStream (console output above).","severity":"low","snippet":"        uint64 end = _addToStream(stream, amount, STREAM_DURATION);","title":"depositRewards(1 wei) restarts the 7-day stream, so anyone can stretch the hook's swap share indefinitely"},{"citation":"resolved","description":"Boundary (external return consumed by caller logic): eligibility re-reads ownerOf at claim time only. Between registration and claim the token can be transferred away and back (sold and rebought, lent, or flash-borrowed from an NFT lending pool) without voiding the registration or restarting the 24-hour lead. An account that never holds Squeeks can flash-borrow 20 of them inside the claimMined transaction (ownerOf == account for the duration of the call), having flash-borrowed them once before for registration, and capture up to the 100,000 HAMSTER global daily cap ahead of real holders (first claim per UTC day wins the global cap). Per-NFT and global caps bound the damage, so this is a boost-fairness issue, not a loss of base mining. The brief states a registration 'is void if ownerOf changes'; the contract cannot observe transfers, so the author should either document this limit or require the token to have been owned continuously by a check the contract can make (for example, require registeredAt <= some on-chain ownership timestamp if the Squeek contract exposes one, or re-require a 24-hour lead whenever ownerOf is seen to differ from the registrant between calls).","line":674,"path":"src/HamsterVault.sol","reproduction":"State: boost active; alice owns token 1 and calls registerSqueek(1) at T. squeek.transferFrom(alice, bob, 1) at T; 5 days later squeek.transferFrom(bob, alice, 1). For the next window k (windowStart(k) >= T + 24h): expected per brief: squeekEligible(1, alice, k) == false (owner changed since registration). Actual: true, and claimMined pays the +50% boost. Reproduced in test/scratch/Misc.t.sol:test_transferOutAndBackKeepsRegistration.","severity":"low","snippet":"        return abi.decode(ret, (address)) == account;","title":"A Squeek registration survives the NFT leaving and returning, so 'void if ownerOf changes' only holds at the two instants checked and a flash-borrowed NFT earns boost"},{"citation":"resolved","description":"The duplicate guard compares only registrant[tokenId] to msg.sender, not the caller's _squeeks list. If alice registered token 1, bob registered it after a transfer, and alice registers it again after it returned, alice's list holds [1, 1]. Boost allocation is safe (the second entry sees capLeft 0 because nftDayUsed is keyed by id) but each duplicate consumes one of the 20 slots and must be removed by a separate unregisterSqueek call per copy. No funds impact.","line":635,"path":"src/HamsterVault.sol","reproduction":"State: boost active, alice owns token 1. alice.registerSqueek(1); transfer 1 to bob; bob.registerSqueek(1); transfer 1 back to alice; alice.registerSqueek(1). Expected: squeeksOf(alice) == [1]. Actual: squeeksOf(alice) == [1, 1] (length 2). Reproduced in test/scratch/Misc.t.sol:test_duplicateListEntry.","severity":"low","snippet":"        if (registrant[tokenId] == msg.sender) revert AlreadyRegistered();","title":"registerSqueek pushes a duplicate tokenId into the caller's list after the token was registered by someone else in between, wasting MAX_SQUEEKS slots"}],"hash":"0b124000fd6d5cbf6e538cad5c04250306ac945216bebcad06f3ac2acb22dc13","nodeId":"291d6410-a37f-4597-87e5-da041f0f16fb","outcome":"completed","summary":"The proof fails on the current tree and passes on the patched copy, so it is a valid proof. The review is complete.\n\n**Outcome:** `.imd-findings.json` holds 5 findings (1 high, 4 low) and a 22-row coverage record covering all 17 verifier-listed entry points plus 5 invariants. Scratch tests live under `test/scratch/` and no project file was changed.\n\n**Findings**\n- **High, `bindToken`:** the remainder check is a strict equality on a balance anyone can move. A 1-wei HAMSTER transfer to the vault between launch 2 and TEAM's bind makes the bind revert forever, and nothing can move the wei out, so the 800M HAMSTER and every claim and stake are bricked. Same for the supply check if the hook token ever burns a unit. Proof test attached; it fails with WrongRemainder now and passes with a `<` check.\n- **Low, `claimMined` boost:** each window in a batch reads the bucket room from a stale `boostCommitted`, so near exhaustion a batched claim reverts where the same windows claimed one at a time succeed.\n- **Low, `depositRewards`:** a 1-wei deposit resets the 7-day stream end. Daily 1-wei deposits leave 2,379 of 7,000 IMD undistributed at the nominal end and 808 at day 14.\n- **Low, Squeek registration:** ownership is checked only at registration and claim, so a token that leaves and returns (including a flash borrow) keeps earning boost with no new 24h lead, against the brief's \"void if ownerOf changes\".\n- **Low, `registerSqueek`:** re-registering a returned token after another holder registered it pushes a duplicate id into the caller's list, wasting slots.\n\n**Verified as holding in my area:** the halving curve (closed form, monotone, bounded, exact at era boundaries, telescoping budgets), floor share plus per-window dust, no overflow in the reward accumulator given IMD's 4.16M mainnet supply, the drip factor to 1e-18, zero-weight pausing of streams and pot, warm-up queue ordering, vesting and unstake cursors. I also confirmed from the game source that `lossOf` is fixed once a window is finalized, so permissionless zero-share claims cannot burn anyone's window.\n\n**Not reached:** behaviour against the real hook token's transfer semantics (launch 2 does not exist yet) and the canonical Squeek collection's `ownerOf` behaviour.","treeHash":null,"usage":{"cachedInputTokens":1282520,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":41117,"runtime":"claude","turns":27,"wallClockMs":606561}},{"artifacts":[],"attempt":1,"bundleHash":"909df312c1d37fc42f385430414d5613e73ef746f0e3e07dc5243c7713f87206","device":"0eca5d6d7ad6ddb4","findings":[{"description":"_allocateBoost takes the 100,000 HAMSTER per-UTC-day global cap from globalDayUsed[day] in the order claims arrive. Because claimMined is permissionless, any caller can choose the order and direct the whole day's boost to some accounts and none to others with equal positions. The brief asks for boost 'allocated deterministically' and for mining 'no claim order changes anyone's amount'; the README acknowledges claim-order allocation but the brief does not offer that choice. A fix that keeps the caps: allocate the global cap pro rata over the day's eligible demand once the day's windows are all finalized (or cap each claim at its pro-rata share of the cap given the day's total eligible base), so the result does not depend on claim order.","line":745,"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\";\n\ncontract Token {\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 a) external {\n        totalSupply += a;\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract Game {\n    uint256 public nextFinalizeWindow;\n    uint256 public cumulativeLoss;\n    mapping(uint256 => uint256) public totalEligibleLoss;\n    mapping(uint256 => uint256) public cumulativeLossBefore;\n    mapping(uint256 => uint256) public cumulativeLossAfter;\n    mapping(uint256 => mapping(address => uint256)) public lossOf;\n\n    function finalizeEmptyTo(uint256 k) external {\n        while (nextFinalizeWindow < k) {\n            cumulativeLossBefore[nextFinalizeWindow] = cumulativeLoss;\n            cumulativeLossAfter[nextFinalizeWindow] = cumulativeLoss;\n            ++nextFinalizeWindow;\n        }\n    }\n\n    function finalize(address a, uint256 la, address b, uint256 lb) external {\n        uint256 k = nextFinalizeWindow;\n        lossOf[k][a] = la;\n        lossOf[k][b] = lb;\n        totalEligibleLoss[k] = la + lb;\n        cumulativeLossBefore[k] = cumulativeLoss;\n        cumulativeLoss += la + lb;\n        cumulativeLossAfter[k] = cumulativeLoss;\n        nextFinalizeWindow = k + 1;\n    }\n}\n\ncontract Nft {\n    mapping(uint256 => address) public ownerOf;\n\n    function mint(address to, uint256 id) external {\n        ownerOf[id] = to;\n    }\n\n    function supportsInterface(bytes4 id) external pure returns (bool) {\n        return id == 0x80ac58cd;\n    }\n}\n\n/// Two wallets, each holding ten registered Squeeks, both lose 20 IMD in the same window. Each is\n/// owed the same boost by every rule in the brief, but the 100,000 HAMSTER global daily cap is\n/// handed out first come, first served: whichever wallet is claimed first (by anyone, since\n/// claimMined is permissionless) takes all of it and the other receives nothing.\ncontract BoostClaimOrderTest is Test {\n    uint64 constant GENESIS = 1791847200;\n    address team = makeAddr(\"team\");\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n    HamsterVault vault;\n    Game game;\n    Nft nft;\n    uint256 k;\n\n    function setUp() public {\n        vm.warp(GENESIS + 10 days);\n        game = new Game();\n        nft = new Nft();\n        vault = new HamsterVault(team, address(game));\n        Token t = new Token();\n        t.mint(address(vault), 8e26);\n        t.mint(address(1), 2e26);\n        vm.prank(team);\n        vault.bindToken(address(t));\n        vm.prank(team);\n        vault.scheduleBoost(address(nft));\n        vm.warp(block.timestamp + 12 hours);\n        vm.prank(team);\n        vault.activateBoost(address(nft));\n        for (uint256 i = 1; i <= 10; ++i) {\n            nft.mint(alice, i);\n            nft.mint(bob, 100 + i);\n            vm.prank(alice);\n            vault.registerSqueek(i);\n            vm.prank(bob);\n            vault.registerSqueek(100 + i);\n        }\n        k = (block.timestamp + 2 days - GENESIS) / 3600;\n        game.finalizeEmptyTo(k);\n        vm.warp(GENESIS + (k + 1) * 3600);\n        game.finalize(alice, 20e18, bob, 20e18);\n    }\n\n    function _claim(address a) internal returns (uint256 boost) {\n        uint256 before = vault.boostCommitted();\n        uint256[] memory ws = new uint256[](1);\n        ws[0] = k;\n        vault.claimMined(ws, a);\n        boost = vault.boostCommitted() - before;\n    }\n\n    function test_boostDoesNotDependOnClaimOrder() public {\n        uint256 snap = vm.snapshotState();\n        uint256 bobWhenFirst = _claim(bob);\n        _claim(alice);\n        vm.revertToState(snap);\n        _claim(alice);\n        uint256 bobWhenSecond = _claim(bob);\n        // identical positions, identical rules: bob's boost must not depend on who was claimed first\n        assertEq(bobWhenSecond, bobWhenFirst, \"bob's boost depends on claim order\");\n    }\n}","reproduction":"Boost active; alice and bob each hold 10 Squeeks registered more than 24h before window k; game window k finalizes with lossOf(k,alice)=lossOf(k,bob)=20 IMD (base 400,000 HAMSTER each, boost wanted 200,000 each, per-NFT caps allow 100,000 each). Claim bob first: bob's boost = 100,000e18, alice's = 0. Claim alice first: bob's boost = 0. Expected: bob's boost is the same in both orders (e.g. 50,000e18 each under a pro-rata split of the 100,000 global cap). Actual: 100,000e18 vs 0.","severity":"medium","title":"Squeek boost global daily cap is first come, first served: claim order changes boost amounts"},{"description":"The brief limits activateBoost to 'the canonical Squeek NFT collection on Ethereum mainnet'. The vault only checks that the address has code and answers ERC-165 for ERC-721, so TEAM can activate any ERC-721 (including one whose minting it controls) and direct up to the 50M boost bucket, within the daily caps and the 50%-of-base rule, to holders of that collection. The README documents this as a trust assumption because the canonical address was not supplied. Once the canonical Squeek address is known, it should be a constant (or constructor argument) that activateBoost compares against.","line":619,"path":"src/HamsterVault.sol","reproduction":"Deploy any contract whose supportsInterface(0x80ac58cd) returns true; TEAM calls scheduleBoost(it), waits 12h, activateBoost(it): succeeds. Expected per brief: revert for anything but the canonical Squeek collection.","severity":"low","title":"activateBoost accepts any ERC-721, not only the canonical Squeek collection"},{"description":"activateBoost has no deadline check. The brief says the 50M moves to mining 'if not activated within 365 days of deploy'; after day 365 the outcome depends on whether sweepBoost or activateBoost lands first. The boost may then be activated outside the window the brief describes, and a sweep becomes impossible.","line":619,"path":"src/HamsterVault.sol","reproduction":"scheduleBoost at DEPLOYED_AT + 364 days + 13h; warp to DEPLOYED_AT + 365 days + 1h (past the deadline); TEAM activateBoost succeeds and sweepBoost then reverts BoostAlreadyActive. Expected: activation refused after DEPLOYED_AT + 365 days, leaving only the sweep.","severity":"low","title":"TEAM can still activate the boost after day 365 until someone sweeps"},{"description":"After sweepBoost, miningBucket is 800M but emissionAt converges to 750M (the brief says the curve is unchanged), so the swept 50M stays in the vault with no path out. This follows the brief literally; the requester may want to know the swept tokens are effectively locked forever.","line":645,"path":"src/HamsterVault.sol","reproduction":null,"severity":"info","title":"Swept boost bucket can never be emitted"},{"description":"Deliberate deviation documented in the README (a 1-wei donation could otherwise block the bind forever). Any surplus is untracked and permanently locked. Requester should confirm the relaxation.","line":226,"path":"src/HamsterVault.sol","reproduction":null,"severity":"info","title":"bindToken accepts balanceOf(vault) >= 8e26, the brief says == 8e26"},{"description":"When an account's warm-up has ended but no transaction has synced past its activation time, pendingRewards (and stakeInfo.pendingRewards) leaves out that stake's earnings since activation. Funds are unaffected (the next sync or claim credits them exactly); the site should call sync() via eth_call or simulate it before showing the figure. Documented in the README.","line":891,"path":"src/HamsterVault.sol","reproduction":"Stake 10 HAMSTER, a 3,000 IMD backlog streaming; warp to activation + 10 days with no transactions; pendingRewards(alice) = 0, after sync() it is ~1,000 IMD.","severity":"info","title":"pendingRewards understates while a due activation has not been synced"}],"hash":"4162b2ad6e418b9825fc3d152d6a1664710a0b5405bfef5054abc2c1d45c3eab","nodeId":"6ee78cc8-16d9-445c-9f84-8935e1371918","outcome":"completed","summary":"I added two test files. The full suite passes under the verifier's fuzz seed: 134 tests, none failing. I found one medium defect: in the Squeek boost, the order in which claims arrive decides who gets the daily boost. I reported it in `.imd-findings.json` with a test that fails on the current code, rather than writing a test that blesses it. I changed nothing outside `test/`, apart from that findings file, which the instructions asked for.\n\n**Tests added**\n- **`test/VaultEdges.t.sol`** (56 tests) covers the failure paths and exact boundaries:\n  - **Bind:** a zero token, a supply or remainder off by one wei, a retry after a failed bind, and a second bind.\n  - **Claims:** an empty batch, a zero beneficiary, a duplicate window that rolls back the whole batch, exactly 48 windows, and a batch containing one unfinalized window.\n  - **Halvings:** one window crossing two halvings.\n  - **Vesting and staking:** vesting to the second, cancelling a pending stake, unstaking with no active stake, and the warm-up and unstake release times one second before and at their boundaries.\n  - **Reward sources:** the 30-day backlog stream and the drip.\n  - **Boost:** the 12-hour timelock, the 365-day sweep, the 20-NFT registration limit, burned tokens, the exact 24-hour registration lead, and the per-NFT daily cap resetting at the UTC day change.\n  - **Fuzz tests** for claim-order independence of base mining across three accounts, window budgets summing to the curve, linear vesting, that calling `sync` more often doesn't change the drip, and that the first staker gets no windfall.\n- **`test/invariant/BoostInvariant.t.sol`** is a random-sequence invariant suite for the boost. It covers registering, transferring and burning NFTs, batch claims, vesting withdrawals and staking. It checks that boost stays within its bucket and the daily caps, never exceeds 50% of base, and that HAMSTER balances and stake weight are fully accounted for.\n  - My first version of this suite never actually earned any boost: the test windows lagged behind the clock. A deliberately failing probe caught it. After the fix, the probe confirmed boost is earned and the global cap gets used.\n- My earlier `VaultInvariant.t.sol` is unchanged.\n\n**Findings in `.imd-findings.json`**\n1. **Medium – boost depends on claim order** (`src/HamsterVault.sol:745`). The 100,000 HAMSTER global daily cap goes to whoever is claimed first. Since anyone can submit claims for anyone, a caller can pick the order. Example: two wallets with identical positions, each holding 10 registered NFTs. Bob receives 100,000 HAMSTER of boost if his claim goes first and 0 if it goes second. The brief says boost should be allocated deterministically. The README acknowledges this behaviour, but the brief doesn't allow it.\n2. **Low:** `activateBoost` accepts any ERC-721 collection, not only the canonical Squeek one. The canonical address was never supplied, so it can't be enforced yet.\n3. **Low:** the team can still activate the boost after day 365, which then blocks the sweep. I confirmed this with a test.\n4. **Info:**\n   - After a sweep, the 50M boost tokens can never be emitted, because the mining curve still stops at 750M. They stay locked in the vault.\n   - `bindToken` accepts a vault balance of at least 8e26, where the brief says exactly 8e26.\n   - `pendingRewards` shows too little until someone calls `sync()` after a warm-up ends.\n\nWhat the tests can't show: the game, IMD and NFT collection are local stand-ins, because nothing here can reach mainnet. I checked the stand-in game against HamsterHour v2's published source, and its interface matches. A fork run against the live contracts is still owed.","treeHash":"2168048e64a3c6730e7220323333a985e7c14069","usage":{"cachedInputTokens":2249141,"inputTokens":48,"model":"claude-opus-5-5","outputTokens":48593,"runtime":"claude","turns":27,"wallClockMs":472121}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30bcdea6dcb9ee40","findings":[{"citation":"resolved","description":"bindToken is the only way to set `token` and the buckets; claimMined, stake, withdrawVested and withdrawUnstaked all revert NotBound until it succeeds. It requires `balanceOf(vault) == 8e26` exactly. Launch 2 delivers exactly 8e26 to the vault and immediately opens a pool, so between that delivery and TEAM's bind transaction (at least one block, in practice longer) any holder of HAMSTER can transfer 1 wei to the vault. The vault has no function that moves HAMSTER out before the bind (withdrawVested/withdrawUnstaked need a bound token, and there is no owner withdrawal by design), so the balance can never return to 8e26 and bindToken reverts WrongRemainder forever. The 800,000,000 HAMSTER remainder is stuck, mining and staking never start. Access x asymmetry: a TEAM-only one-shot whose precondition is a storage value anyone can push off the exact target. Minimal fix that preserves the design: require `balanceOf(vault) >= EXPECTED_REMAINDER` (excess is untracked dust the same way miningDust is), keep the totalSupply check. The same strict `totalSupply() != 1e27` check is a second fragility if the hook token ever burns (e.g. part of the anti-snipe tax); if burns are possible, check `<=` or the vault's share instead.","line":223,"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\";\n\n/// @dev Minimal ERC-20 standing in for the launch-2 HAMSTER token: 1e27 supply, 8e26 delivered to the vault.\ncontract HamsterToken {\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 Anyone holding 1 wei of HAMSTER can permanently prevent `bindToken`, and with it every claim,\n/// stake and grant withdrawal, by sending that wei to the vault before TEAM binds. There is no path that\n/// moves HAMSTER out of an unbound vault, so the 800,000,000 remainder is stuck forever.\ncontract BindDonationDoSTest is Test {\n    address team = makeAddr(\"team\");\n    address game = makeAddr(\"game\");\n    address attacker = makeAddr(\"attacker\");\n    address pool = makeAddr(\"pool\");\n\n    HamsterVault vault;\n    HamsterToken hamster;\n\n    function setUp() public {\n        vault = new HamsterVault(team, game);\n        hamster = new HamsterToken();\n        // Launch 2: 1B supply, exactly 800M to the vault (remainderTo), 200M to the pool.\n        hamster.mint(address(vault), 8e26);\n        hamster.mint(pool, 2e26);\n        // The attacker buys (here: receives) 1 wei of HAMSTER from the pool.\n        vm.prank(pool);\n        hamster.transfer(attacker, 1);\n    }\n\n    /// @dev Expected: TEAM can bind once the vault holds at least the 800M remainder, so a stray wei\n    /// donated by a third party cannot lock the vault. Actual: the strict `== 8e26` check reverts\n    /// WrongRemainder forever, because no function can move HAMSTER out before the bind.\n    function test_donationBeforeBindDoesNotLockVaultForever() public {\n        // Attacker front-runs TEAM's bind with a 1 wei donation.\n        vm.prank(attacker);\n        hamster.transfer(address(vault), 1);\n        assertEq(hamster.totalSupply(), 1e27);\n        assertEq(hamster.balanceOf(address(vault)), 8e26 + 1);\n\n        vm.prank(team);\n        vault.bindToken(address(hamster)); // reverts WrongRemainder on the current code\n\n        assertEq(address(vault.token()), address(hamster));\n        assertEq(vault.miningBucket(), 750_000_000e18);\n        assertEq(vault.boostBucket(), 50_000_000e18);\n    }\n}","reproduction":"State: HamsterVault deployed, HAMSTER token with totalSupply 1e27 and balanceOf(vault) == 8e26, pool holds 2e26, attacker holds 1 wei bought from the pool. Sequence: (1) attacker: hamster.transfer(vault, 1) -> balanceOf(vault) = 8e26 + 1. (2) TEAM: vault.bindToken(hamster). Expected: bind succeeds (vault holds at least the remainder). Actual: revert WrongRemainder(); no call can ever reduce the vault's balance, so every later bindToken reverts too and claimMined/stake/withdrawVested revert NotBound forever. Run `forge test --match-path test/scratch/BindDonationDoS.t.sol`.","severity":"high","snippet":"        if (IERC20(token_).balanceOf(address(this)) != EXPECTED_REMAINDER) revert WrongRemainder();","title":"bindToken strict balance equality lets anyone permanently lock the vault with a 1 wei HAMSTER donation"},{"citation":"resolved","description":"The brief restricts activateBoost to 'the canonical Squeek NFT collection on Ethereum mainnet'. The contract only checks that the scheduled address has code and answers ERC-165 for 0x80ac58cd. The README documents this as a TEAM-supplied setting because the Squeek address was unavailable, so this is a trust assumption rather than a bypass: TEAM (one-shot, 12h timelock) can direct the entire 50M boost bucket to holders of any collection, for example one it deploys and mints to itself, then register up to 20 tokens per wallet and earn +50% on its own mined base up to the daily caps. Document it to the judge as the single material TEAM power; if the canonical address becomes known before deploy, pin it as a constant or constructor argument.","line":599,"path":"src/HamsterVault.sol","reproduction":"Sequence: TEAM deploys MockERC721 `mine` (supportsInterface(0x80ac58cd) == true); TEAM: scheduleBoost(mine); warp +12h; TEAM: activateBoost(mine) succeeds and collection == mine; mine.mint(TEAM, 1); TEAM: registerSqueek(1) succeeds. Expected per brief: only the canonical Squeek collection can be activated. Actual: any ERC-165-compliant ERC-721 is accepted (test_teamCanActivateArbitraryCollection in test/scratch/Checks.t.sol).","severity":"low","snippet":"        if (collection_.code.length == 0) revert BadCollection();\n        (bool ok, bytes memory ret) =\n            collection_.staticcall(abi.encodeCall(IERC721.supportsInterface, (ERC721_INTERFACE)));\n        if (!ok || ret.length < 32 || !abi.decode(ret, (bool))) revert BadCollection();\n        collection = IERC721(collection_);","title":"Boost collection is not pinned: TEAM can activate the 50M boost for any ERC-721, including one it controls"},{"citation":"resolved","description":"Boost eligibility is evaluated when the window is claimed, not for the window's own time: squeekEligible requires ownerOf(tokenId) == account now. Eligibility resumes when the token returns to the registrant (registeredAt is kept), so ownership change is not in itself permanent. But claimMined may be called by anyone for any account and marks (k, account) claimed with whatever boost was allocated at that moment. While the registrant's Squeek is in an escrow, lending or marketplace contract, any address can claim the registrant's finalized windows and permanently fix their boost at zero; waiting until the NFT came back would have paid 10,000 HAMSTER per NFT per day. The base share is unaffected. This is an asymmetry between the permissionless claim trigger and a claim-time eligibility snapshot. Mitigation options that keep the design: let only the account (or an approved operator) claim windows that currently carry a non-zero base when the boost is active, or let the registrant claim the boost part separately later.","line":672,"path":"src/HamsterVault.sol","reproduction":"State: boost active; alice owns Squeek #1 and registered it at T; window k starts at T+2d with alice's lossOf = 10 IMD (base 200,000 HAMSTER, boost 10,000). After k is finalized alice transfers #1 to an escrow address. Bob calls claimMined([k], alice). Expected: alice's boost for k is preserved or claimable later by her. Actual: claimableMined(k, alice) reports boost 0 while escrowed, the claim records base 200,000 and boost 0, claimed[k][alice] = true; after #1 returns to alice claimableMined is (0,0) and the 10,000 boost is unrecoverable (test_thirdPartyClaimDuringEscrowZeroesBoostForever in test/scratch/Checks.t.sol).","severity":"low","snippet":"        (bool ok, bytes memory ret) = address(collection).staticcall(abi.encodeCall(IERC721.ownerOf, (tokenId)));\n        if (!ok || ret.length < 32) return false;\n        return abi.decode(ret, (address)) == account;","title":"Permissionless claimMined snapshots Squeek ownership at claim time, so a third party can zero a user's boost forever by claiming while the NFT is temporarily elsewhere"},{"citation":"resolved","description":"The brief asks for boost 'allocated deterministically' alongside mining where 'no claim order changes anyone's amount'. The global 100,000 per-day cap (and the per-NFT 10,000 cap shared between successive registrants of the same tokenId) is consumed in claim order. With more than ten eligible NFTs active in one day, the accounts that claim first take the whole cap and later claimants, whose base share is identical, receive zero boost. Bots that claim immediately after finalization (claimMined is permissionless, so they can even claim for themselves the moment nextFinalizeWindow advances) systematically win over ordinary users. If order independence is intended, the cap must be applied pro-rata per window (e.g. scale each account's wanted boost by cap / total eligible wanted for the day), which requires knowing the day's total demand; otherwise document the first-come rule.","line":682,"path":"src/HamsterVault.sol","reproduction":"State: boost active; 11 accounts each registered one Squeek 24h+ before window k; each loses 10 IMD in k (base 200,000, wanted boost 100,000, per-NFT cap 10,000). Before any claim claimableMined reports 10,000 boost for all 11. The first ten accounts claim and receive 10,000 each (globalDayUsed = 100,000). Expected: the 11th account, with the same loss, receives the same boost. Actual: claimableMined(k, acc11) boost is 0 and its claim yields no boost (test_boostGlobalCapIsFirstCome in test/scratch/Checks.t.sol).","severity":"low","snippet":"        uint256 left = BOOST_GLOBAL_DAILY_CAP - globalDayUsed[day];\n        uint256 bucketLeft = boostBucket - boostCommitted;\n        if (left > bucketLeft) left = bucketLeft;\n        if (wanted > left) wanted = left;","title":"Boost daily caps are first-come: once a UTC day's demand exceeds 100,000 HAMSTER, claim order decides who gets boost"},{"citation":"resolved","description":"depositRewards is permissionless and every deposit merges the leftover into a fresh 7-day stream whose end is now + 7 days regardless of how much was left or how soon the previous stream would have finished. A griefer who deposits 1 wei of IMD once a day keeps the remaining pool asymptotically undistributed: each restart pays only ~1/7 of the leftover over the next day. Rewards are never lost and the hook's own frequent deposits already imply a rolling 7-day schedule, so impact is a delay, not a theft. A weighted merge (new end = weighted average of the old end and now + 7 days by amount, or a minimum deposit for restarting the end) removes the cheap lever.","line":485,"path":"src/HamsterVault.sol","reproduction":"State: alice is the only staker with active weight; 700 IMD deposited via depositRewards at T (stream end T+7d). At T+6d, 100 IMD remain to be paid in the next day. Bob calls depositRewards(1). Expected: the 100 IMD still finish within the day. Actual: stream.remaining = 100e18 + 1 and stream.end = now + 7 days; one day later alice has accrued only ~14.3 of the 100 IMD (pendingRewards 614.28 IMD instead of 700) (test_dustDepositRestartsStream in test/scratch/Checks.t.sol).","severity":"low","snippet":"    function _addToStream(Stream storage s, uint256 amount, uint64 duration) private returns (uint64 end) {\n        s.remaining += amount;\n        end = uint64(block.timestamp) + duration;\n        s.end = end;\n    }","title":"Anyone can restart the 7-day reward stream with a 1 wei depositRewards, indefinitely delaying stakers' IMD"},{"citation":"resolved","description":"Every stake pushes a queue entry even when it merges into an existing warm-up, and _sync reverts when more than MAX_SYNC (64) entries are due. One account can create 65 entries with 65 stakes of MIN_STAKE in different blocks (65 HAMSTER total, later recoverable through requestUnstake). 24 hours later claimMined, claimRewards, stake, requestUnstake, cancelPendingStake, withdrawVested, depositRewards, depositDrip and pullFromGame all revert SyncRequired until anyone calls sync(n). The README documents sync(n) as the remedy and the public sync is cheap, so this is a griefing lever (a few blocks of outage per repetition, roughly 65 transactions of attacker cost each time) rather than a lockout. Dropping stale entries without counting them toward MAX_SYNC, or not pushing a new entry when an account's pendingActivateAt is already queued for the same second, would raise the cost.","line":497,"path":"src/HamsterVault.sol","reproduction":"State: token bound. Bob stakes 1e18 in 65 consecutive seconds (65 queue entries). Warp +1 day. Expected: alice's stake(10e18) and pullFromGame proceed. Actual: both revert SyncRequired(); after vault.sync(65) the same calls succeed (test_syncRequiredGriefing in test/scratch/Checks.t.sol).","severity":"low","snippet":"        if (queueHead < queueTail && _queue[queueHead].activateAt <= block.timestamp) revert SyncRequired();","title":"65 one-HAMSTER stakes maturing together make every state-changing call revert SyncRequired until someone calls sync"}],"hash":"4ef4005911feaa933057338688a48a3516343e0f74349646d6f495fae9f2fd2d","nodeId":"ea5d632e-ebf3-41a8-bf4c-d9a7e40a617e","outcome":"completed","summary":"The review is complete. The findings file is written and validated, no repository file was changed, and the scratch tests live only under the ignored `test/scratch/` directory.\n\n**Outcome.** One high-severity defect and five low-severity issues, all reproduced with tests, recorded in `.imd-findings.json` with a coverage row for every one of the 17 entry points plus three invariant rows.\n\n**The high finding (with Foundry proof).** The bind step can be locked forever by anyone. `bindToken` requires the vault's HAMSTER balance to equal exactly 800M. Launch 2 opens a pool the moment it delivers that balance, so any buyer can transfer 1 wei to the vault before TEAM's bind transaction. Nothing can move HAMSTER out of an unbound vault, so every later bind reverts and claims, staking and grant withdrawals never open. The proof test fails on the current code with `WrongRemainder` and passes when the check becomes `>=`. I verified both directions and restored the source afterwards.\n\n**Low findings, each with a reproducing test.**\n- The boost collection is not pinned to the canonical Squeek address. TEAM can activate any ERC-165 ERC-721, including one it controls. The README already documents this trust assumption.\n- Boost eligibility is snapshotted at claim time and claims are permissionless. A third party can claim a user's window while their Squeek sits in escrow, permanently fixing that window's boost at zero.\n- Boost daily caps are first-come. Once a day's demand exceeds 100,000 HAMSTER, the eleventh identical claimant gets nothing, which conflicts with the brief's \"allocated deterministically\".\n- A 1 wei `depositRewards` restarts the 7-day stream, so a griefer can keep delaying stakers' IMD indefinitely. Rewards are delayed, never lost.\n- 65 one-HAMSTER stakes maturing together make every state-changing call revert `SyncRequired` until anyone calls `sync`. Documented remedy exists, so this is a griefing lever.\n\n**What held.** I traced the mining curve and bucket conservation against the real game source, confirmed the interface and window geometry match HamsterHour v2 on GitHub, and checked warm-up merging, cancel-then-restake in the same second, sweep before and after bind, unstake mirroring and the zero-weight pause logic. All behaved as the brief intends.\n\n**Not reached.** I did not review the ABI export or the real IMD token's transfer semantics on mainnet, and the TEAM address in the deploy params could not be verified against the launch wallet from here.","treeHash":null,"usage":{"cachedInputTokens":2189148,"inputTokens":738,"model":"claude-fable-5-1","outputTokens":39480,"runtime":"claude","turns":24,"wallClockMs":797693}},{"artifacts":[],"attempt":1,"bundleHash":"158f88c2a92d795be8ab8587193d957c563470c5f116c64c5e90d92dda8e2778","device":"4f2ce49dc630c86e","findings":[],"hash":"4f9f93683c5530c5d4aab46591ab584088f403ebdd34c0afd7360836352bd516","nodeId":"a6d0a62f-3c54-4b77-8a37-a552aed64ac8","outcome":"completed","summary":"I fixed six of the eight findings, including all three that reopened the work, and disputed the other two. `forge build`, `forge fmt --check` and `forge test` all pass with the pinned seed: 77 tests, including the reviewer's proof. All eight answers are in `.imd-responses.json`.\n\n**Fixed (the three that blocked the launch)**\n- **Donation locking the bind (high):** `bindToken` now only requires the vault to hold *at least* 800M HAMSTER, not exactly 800M. A 1-wei donation can no longer block it. The buckets stay 750M mining and 50M boost, and any extra is left untracked. I kept the exact 1e27 supply check because the brief states it. That means launch 2's token must not burn any supply before the bind; the README says so.\n- **65 due warm-ups blocking every call (medium):** confirmed in the code. Staking no longer adds one queue entry per stake:\n  - Warm-ups now end on the next game-window boundary, so the warm-up is 24 to 25 hours instead of exactly 24.\n  - Pending stakes are grouped by that end time, and each account's stake becomes active the next time the account is touched. It still earns from its exact activation time.\n  - Because every stake also catches up the queue, at most about 26 end times can be due at once. So the catch-up never reverts, and the hook's deposits can't be blocked.\n  - The 64-entry limit, the `SyncRequired` error and `sync(max)` are gone; `sync()` is now an optional refresh.\n- **Squeek registration surviving a transfer (medium):** reproduced. Once the vault sees that the registrant no longer owns the NFT, it deletes the registration for good, and the old holder has to register again and wait another 24 hours. The vault checks this during claims and when a new owner registers. Anyone can also trigger it with a new function, `voidSqueek(tokenId)`. One limit remains and is in the README: a transfer that leaves and comes back between two checks, including a flash loan inside one transaction, can't be seen by any contract.\n\n**Fixed (advisory)**\n- **Batch claims near the end of the boost bucket:** each window in a batch now counts the boost already given earlier in the same batch, so the batch is capped instead of reverting.\n- **Dust deposits stretching the 7-day stream:** a new deposit now moves the stream's end in proportion to its size, so 1-wei deposits barely move it.\n- **Duplicate Squeek entries:** registering now first removes duplicates and tokens someone else has since registered from the caller's list.\n\n**Disputed**\n- **Global daily boost cap is first-come:** it reproduces, but the brief's \"no claim order changes anyone's amount\" is in the mining section, and base mining is order-independent. For the boost the brief asks for \"deterministic\", which first-come is. Splitting the cap pro rata would need a settlement round after each day closes, which the brief doesn't ask for.\n- **Any ERC-721 can be activated as the Squeek collection:** true, but the canonical collection's address isn't given anywhere I can use, and guessed addresses aren't allowed. The 12-hour public timelock remains the safeguard, and the README lists this as a trust assumption.\n\nI also added tests for each fix, updated three existing tests to the new behaviour, regenerated `abi/HamsterVault.json` and updated the README. The contract's runtime size is 21,052 bytes, under the 24,576 limit. Nothing is committed.","treeHash":"4abce6093e32aae84d67d0217f46d72b4f6370d8","usage":{"cachedInputTokens":1617446,"inputTokens":38,"model":"claude-opus-5-5","outputTokens":30355,"runtime":"claude","turns":20,"wallClockMs":386661}},{"artifacts":[],"attempt":1,"bundleHash":"f7ea0bcc82dd77aa925677070e464a1956505a2e439c187e8958e890c4e39bf9","device":"d32559488518f947","findings":[],"hash":"6016085ebb3a5dae59eeb67eaea4a7e80226f2387e8c5f00c01de0dfe570343c","nodeId":"a6d0a62f-3c54-4b77-8a37-a552aed64ac8","outcome":"completed","summary":"The work is complete and committed. All checks pass: `forge build`, `forge test` with the verifier's fuzz seed (66 tests, 0 failures), and `forge fmt --check`.\n\n**What was built**\n\n`src/HamsterVault.sol` is one contract, 19,425 bytes of runtime, with constructor arguments `team_` ($owner) and `game_` (HamsterHour v2). It has no owner, pause, upgrade or withdrawal path. TEAM holds only the one-shot `bindToken` and the 12-hour timelocked `scheduleBoost` / `activateBoost`.\n\n- **Mining** uses a closed-form emission curve: 20,000 HAMSTER per IMD, halving every 18,750 IMD, converging to the 750M bucket. Window budgets are the curve difference across the game's cumulative loss bounds, so halvings crossed inside a window are exact and budgets telescope to never exceed the bucket. Shares round down and whole-token rounding dust goes to a tracked `miningDust` bucket.\n- **Vesting** creates one 7-day linear grant per claim. Grant balances count as stake weight immediately and are never transferable.\n- **Staking** keeps a FIFO queue of warm-up activations and replays them in time order before every state change, so a stake earns from its exact 24-hour mark without any keeper. Unstake stops weight immediately and releases after 24 hours. Three IMD sources: game pulls (first pull streams over 30 days, later ones over 7), hook deposits streamed over 7 days, and a continuous 1%-per-day drip. All sources pause when total weight is zero, so there is never a first-staker windfall.\n- **Boost** is dormant until TEAM activates it for the Squeek collection. Registration is by ownership check only, needs a 24-hour lead before a window, is void on transfer, and is capped at 10,000 per NFT and 100,000 globally per UTC day. Anyone can sweep the 50M into the mining bucket after 365 days without activation.\n\n**Tests** in `test/` cover bind checks, retroactive claims, halving crossing, claim-order independence, fuzzed conservation of both buckets, dust, vesting as stake, warm-up exactness, unstake timing, zero-staker gaps, drip math and non-pokeability, pro-rata splits, backlog streaming, boost registration, transfer voiding, both daily caps, and the 365-day sweep. The mock game reproduces the v2 loss-history and pull-balance interface.\n\n**One open item for TEAM.** The canonical Squeek NFT collection address was not in the brief and could not be verified here, so it is supplied at `scheduleBoost` time. The vault checks only that it is an ERC-721 contract, and the timelock makes the choice reviewable. The README lists this under \"After launch\" along with the game's `scheduleStakingRecipient(vault)` step, the bind, and the hook wiring for launch 2.","treeHash":"8e5cb628a0bf363dc4bf588992f6f11554b115e6","usage":{"cachedInputTokens":2781149,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":79578,"runtime":"claude","turns":47,"wallClockMs":1023533}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2c968e88904ec22b","findings":[{"citation":"resolved","description":"Launch 2 delivers 8e26 HAMSTER to the vault at token deployment and trading on the v4 pool starts at once, so there is at least one block between the remainder arriving and TEAM's bindToken transaction. Any holder of HAMSTER (anyone who buys from the pool, or a mempool front-runner sandwiching TEAM's bindToken) can transfer 1 wei to the vault. From then on balanceOf(vault) is 8e26+1, the strict equality on line 223 can never hold again, and there is no path to move tokens out of the vault before bind (no owner, no withdrawal, claims/stakes all require NotBound to be false). Result: bindToken reverts WrongRemainder permanently, 800,000,000 HAMSTER are locked, mining can never be claimed and staking never opens. The same strict check on totalSupply (line 222) would brick bind if the hook token ever burns (e.g. a burn component of the anti-snipe tax) before bind. Fix that preserves the brief: check balanceOf(vault) >= EXPECTED_REMAINDER (and totalSupply within the expected shape), and set the buckets from the constants as now; any excess simply stays untracked in the vault (trackedHamster already tolerates this after bind).","line":223,"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\";\n\n/// @dev Minimal ERC-20 with the HAMSTER launch shape: 1e27 total, 8e26 delivered to the vault.\ncontract Hamster {\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 Fails on the current code: a 1 wei HAMSTER transfer to the vault before `bindToken`\n/// makes `balanceOf(vault) != 8e26`, so `bindToken` reverts `WrongRemainder` forever and the\n/// 800,000,000 HAMSTER (and all mining) are locked. Passes once the remainder check tolerates\n/// extra balance (e.g. `>= EXPECTED_REMAINDER`, buckets still set from the constants).\ncontract BindDonationTest is Test {\n    address team = address(0xA11CE);\n    address game = address(0x6A3E);\n    address griefer = address(0xBAD);\n\n    function test_oneWeiDonationBeforeBindMustNotBrickTheVault() public {\n        HamsterVault vault = new HamsterVault(team, game);\n        Hamster hamster = new Hamster();\n        // Launch 2 shape: remainderTo = vault gets exactly 8e26, the rest (pool etc.) 2e26.\n        hamster.mint(address(vault), 8e26);\n        hamster.mint(griefer, 2e26);\n\n        // Anyone holding 1 wei of HAMSTER (bought from the pool) front-runs TEAM's bindToken.\n        vm.prank(griefer);\n        hamster.transfer(address(vault), 1);\n        assertEq(hamster.totalSupply(), 1e27);\n        assertEq(hamster.balanceOf(address(vault)), 8e26 + 1);\n\n        // Expected: TEAM can still bind (the vault holds at least the remainder) and the buckets\n        // are the documented 750M / 50M. Actual: WrongRemainder, permanently.\n        vm.prank(team);\n        vault.bindToken(address(hamster));\n        assertEq(address(vault.token()), address(hamster));\n        assertEq(vault.miningBucket(), 750_000_000e18);\n        assertEq(vault.boostBucket(), 50_000_000e18);\n    }\n}","reproduction":"State: fresh HamsterVault(team, game); HAMSTER token with totalSupply 1e27, 8e26 minted to the vault, 2e26 elsewhere. Step 1: a holder transfers 1 wei HAMSTER to the vault (hamster.transfer(vault, 1)). Step 2: team calls bindToken(hamster). Expected: bind succeeds, miningBucket 750M, boostBucket 50M. Actual: revert WrongRemainder(), and every later bindToken call reverts the same way forever. See test/scratch/BindDonation.t.sol.","severity":"critical","snippet":"        if (IERC20(token_).balanceOf(address(this)) != EXPECTED_REMAINDER) revert WrongRemainder();","title":"bindToken requires balanceOf(vault) == 8e26 exactly: a 1 wei HAMSTER donation before bind bricks the vault forever"},{"citation":"resolved","description":"Every stake() pushes one Activation entry (line 375) even when the same wallet re-stakes and the previous entry becomes stale. _sync() processes at most MAX_SYNC = 64 due entries and then reverts SyncRequired if one more is due (line 497), rolling back the 64 it processed. All of stake, requestUnstake, cancelPendingStake, claimRewards, claimMined, withdrawVested, depositRewards, depositDrip and pullFromGame go through _sync(). One wallet calling stake(1e18) 65 times in one block (about 100k gas each, 65 HAMSTER that it gets back via cancelPendingStake + withdrawUnstaked) therefore makes all of them revert from 24h later until some third party calls sync(n). depositRewards and depositDrip are what launch 2's hook calls from inside swaps; unless the hook wraps the call in try/catch, every swap on the HAMSTER pool reverts during that time, and the attacker can re-arm a fresh batch every block. Even below the threshold the first caller after a batch pays for up to 64 activations (about 2.5M gas) inside their own tx or swap. Fix: do not let the hook-facing deposits depend on draining the backlog (e.g. buffer deposits and merge them at the next successful sync), and/or keep one live queue entry per account (drop the stale entry on re-stake) and raise MAX_SYNC.","line":497,"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\";\n\ncontract Token {\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        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @notice Fails on the current code: one wallet makes 65 one-HAMSTER stakes in one block; 24h\n/// later every state-changing entry point, including the hook-facing `depositRewards` and\n/// `depositDrip`, reverts `SyncRequired` until a third party calls `sync`. Passes once the\n/// hook deposit path no longer depends on draining the whole activation backlog (or the backlog\n/// cannot be inflated this cheaply).\ncontract SyncDosTest is Test {\n    address constant IMD_ADDR = 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7;\n    address team = address(0xA11CE);\n    address game = address(0x6A3E);\n    address attacker = address(0xBAD);\n    address hook = address(0x4004);\n\n    HamsterVault vault;\n    Token hamster;\n    Token imd;\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        Token impl = new Token();\n        vm.etch(IMD_ADDR, address(impl).code);\n        imd = Token(IMD_ADDR);\n        vault = new HamsterVault(team, game);\n        hamster = new Token();\n        hamster.mint(address(vault), 8e26);\n        hamster.mint(address(0xBEEF), 2e26);\n        vm.prank(team);\n        vault.bindToken(address(hamster));\n    }\n\n    function test_hookDepositMustNotBeBlockedByCheapActivationBacklog() public {\n        hamster.mint(attacker, 65e18);\n        vm.startPrank(attacker);\n        hamster.approve(address(vault), type(uint256).max);\n        for (uint256 i; i < 65; ++i) {\n            vault.stake(1e18); // 65 queue entries, all due at the same second\n        }\n        vm.stopPrank();\n\n        vm.warp(block.timestamp + 24 hours);\n\n        // The hook's swap-share deposit (made inside a swap) must go through.\n        imd.mint(hook, 100e18);\n        vm.startPrank(hook);\n        imd.approve(address(vault), type(uint256).max);\n        vault.depositRewards(50e18); // actual: reverts SyncRequired\n        vault.depositDrip(50e18);\n        vm.stopPrank();\n        assertEq(imd.balanceOf(address(vault)), 100e18);\n    }\n}","reproduction":"State: token bound. attacker: approve, then stake(1e18) x65 in one block. warp +24h. hook: approve IMD, depositRewards(50e18). Expected: deposit accepted and streamed. Actual: revert SyncRequired(); depositDrip, stake, claimMined, claimRewards, pullFromGame all revert the same way until someone calls sync(>=1). See test/scratch/SyncDos.t.sol.","severity":"medium","snippet":"        if (queueHead < queueTail && _queue[queueHead].activateAt <= block.timestamp) revert SyncRequired();","title":"Sixty-five one-HAMSTER stakes from one wallet make every state-changing call, including the hook's depositRewards/depositDrip, revert SyncRequired 24h later"},{"citation":"resolved","description":"The brief says a registration 'is void if ownerOf changes'. squeekEligible only compares the current ownerOf with the registrant at the moment claimMined runs (lines 672-674); nothing records that the token changed hands in between, and nothing voids the registration. Two consequences. (1) A registrant who sells or lends the NFT keeps earning boost for every window during which someone else held it, as long as it is back in the wallet for the one block of the claim (claims are permissionless and batch up to 48 windows, so one borrow covers weeks). (2) A wallet that never holds the NFT can borrow it for one transaction to call registerSqueek, return it, and 24h later borrow it again for one transaction to claim; the real holder, if they never register, is silently disenfranchised and the 50M boost bucket pays non-holders. Per-NFT and global daily caps bound the amount per day but not the mechanism. Fix: make the void permanent and observable, e.g. a permissionless voidSqueek(tokenId) that deletes the registration whenever ownerOf != registrant (and have claimMined/registerSqueek do the same when they observe it), so that a re-registration needs a fresh 24h lead; document that only transfers observed on-chain can be enforced.","line":672,"path":"src/HamsterVault.sol","reproduction":"State: boost active; alice owns Squeek #1 and registers it 2 days before window 300 starts; alice transfers #1 to bob before window 300 and bob holds it through the window; alice loses 1 IMD in window 300; window finalized. 5 days later alice gets #1 back for one block and calls claimMined([300], alice). Expected (brief): registration void since ownerOf changed, boost 0, grant 20,000 HAMSTER. Actual: claimableMined returns boost 10,000e18 and the grant is 30,000e18, boostCommitted 10,000e18. The second scenario (carol holds, alice flash-borrows at register and claim) gives alice the same 30,000e18 grant. Both are in test/scratch/BoostTransient.t.sol (they pass on the current code, demonstrating the behavior).","severity":"medium","snippet":"        (bool ok, bytes memory ret) = address(collection).staticcall(abi.encodeCall(IERC721.ownerOf, (tokenId)));\n        if (!ok || ret.length < 32) return false;\n        return abi.decode(ret, (address)) == account;","title":"Boost registration is only checked against ownerOf at claim time, so an NFT not held during the window (sold, lent or flash-borrowed) still earns the registrant the boost"},{"citation":"resolved","description":"Inside claimMined the per-window boost is clamped to bucketLeft = boostBucket - boostCommitted, but boostCommitted is only increased after the loop (line 274). Every window in the same batch therefore sees the same pre-batch bucketLeft, their boosts can sum to more than it, and the post-loop check on line 275 reverts the whole batch with BucketExceeded. nftDayUsed and globalDayUsed are updated per window (lines 693, 696), so only the bucket clamp is stale. The caller can work around it by claiming one window per call, where the clamp is exact. Fix: track the running boost total inside the loop (pass bucketLeft - boost into _allocateBoost, or update boostCommitted per window).","line":683,"path":"src/HamsterVault.sol","reproduction":"State: boost active, boostBucket - boostCommitted = 15,000e18 (reachable after the bucket has paid out for about 500 days at the 100k/day cap). alice has one registered eligible NFT and two finalized windows 300 and 301 on different UTC days, each with base 20,000e18 (wants 10,000e18 boost each). claimMined([300, 301], alice). Expected: boost clamped to 10,000e18 + 5,000e18 and the claim succeeds. Actual: each window allocates 10,000e18 against the stale bucketLeft of 15,000e18, boostCommitted would become bucket + 5,000e18, revert BucketExceeded(); claimMined([300], alice) then claimMined([301], alice) succeeds with the clamped amounts. Confirmed in test/scratch/BatchBucket.t.sol (windows 300 and 324, boostCommitted set to bucket - 15,000e18 via vm.store slot 4: the batch reverts BucketExceeded, single-window claims grant 30,000e18 and 25,000e18).","severity":"low","snippet":"        uint256 bucketLeft = boostBucket - boostCommitted;","title":"_allocateBoost reads boostCommitted that is only updated after the batch loop, so a multi-window claim near the end of the boost bucket reverts BucketExceeded instead of clamping"},{"citation":"resolved","description":"The brief restricts activateBoost to the canonical Squeek NFT collection on Ethereum mainnet. The code accepts whatever address TEAM schedules, provided it has code and answers supportsInterface(0x80ac58cd) true. The README states this and the 12h timelock makes the choice reviewable; no address was available to pin. Recorded as a trust assumption, not a defect: TEAM scheduling a collection it controls would let it mint NFTs to its own wallets and direct up to 100,000 HAMSTER per day of the 50M boost bucket to them. If the canonical address is known at deploy time it can be made a constant checked in scheduleBoost.","line":583,"path":"src/HamsterVault.sol","reproduction":"TEAM calls scheduleBoost(X) for any contract X whose supportsInterface(0x80ac58cd) returns true, waits 12h, calls activateBoost(X): accepted. No input makes the contract distinguish X from the canonical Squeek collection.","severity":"info","snippet":"    function scheduleBoost(address collection_) external onlyTeam {\n        if (collection_ == address(0)) revert ZeroAddress();","title":"The vault cannot enforce that the boost collection is the canonical Squeek contract; any ERC-165 ERC-721 passes (documented trust assumption)"}],"hash":"60e52ac6f137d30c047e8ffb165880fdb315228c8007b662ffdabf0b9e56e725","nodeId":"b1e26c5f-8fc5-40cb-865c-89d17bafff99","outcome":"completed","summary":"The review is complete. The findings file `.imd-findings.json` holds 5 findings and 20 coverage rows covering all 17 listed entry points. No repository files were changed; the scratch tests live under the gitignored `test/scratch/` directory.\n\n**Findings**\n\n1. **Critical: `bindToken` can be bricked forever by a 1 wei donation.** The remainder check at `src/HamsterVault.sol:223` requires `balanceOf(vault) == 8e26` exactly. Anyone holding HAMSTER after launch 2 can transfer 1 wei to the vault before TEAM binds, and no path can ever move it out. The proof test fails with `WrongRemainder` on the current code.\n2. **Medium: cheap activation backlog blocks the hook.** One wallet staking 1 HAMSTER 65 times in a block makes every state-changing call revert `SyncRequired` 24 hours later, including `depositRewards` and `depositDrip`, which the hook calls inside swaps. The proof test fails with `SyncRequired`.\n3. **Medium: boost ownership is only checked at claim time.** The brief says a registration is void once `ownerOf` changes, but `squeekEligible` only polls ownership during `claimMined`. An NFT sold, lent or flash-borrowed through a whole window still earns the registrant the boost. Both repro scenarios pass on the current code, demonstrating the behavior.\n4. **Low: batched claims near the end of the boost bucket revert instead of clamping**, because `_allocateBoost` reads a `boostCommitted` that is only updated after the loop. Confirmed with a storage-set scratch test.\n5. **Info: the canonical Squeek restriction is a trust assumption.** Any ERC-165 ERC-721 passes `scheduleBoost`, as the README already states.\n\n**What held**\n\n- Mining curve, window budgets, dust tracking, claim-order independence and both bucket bounds. The emission curve is monotone across every era boundary.\n- The game interface matches the real HamsterHour v2 source and the live mainnet constants. `lossOf` is immutable once a window is finalized, so a third-party zero-share claim cannot burn a later entitlement.\n- Vesting, warm-up replay, unstake timing, zero-weight pausing and the drip math behave as the brief asks.\n\n**Not reached in depth**\n\n- The HAMSTER token itself and the hook integration belong to launch 2 and were only reasoned about from the brief.\n- The IMD token's bytecode was not inspected for transfer hooks beyond confirming it is a standard-looking ERC-20 on mainnet.","treeHash":null,"usage":{"cachedInputTokens":1579201,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":50862,"runtime":"claude","turns":34,"wallClockMs":722932}},{"artifacts":[],"attempt":1,"bundleHash":"cf10f853d7e21296c2b2d89f4020fdc186581a273ff908fff95a5ce7e152ee5c","device":"d00f790fc692b1a4","findings":[],"hash":"63b51e740b8daa8f01cb7d20e0f18857fb496de25df77eac1b553bc0c258d244","nodeId":"dc184581-38e1-4c78-8753-cf804c689963","outcome":"completed","summary":"The manifest at `launch.json` is complete and verified. No revision findings were supplied in this round, and the existing file already matches the accepted implementation, so I left it unchanged.\n\n**What I checked**\n\n- The manifest has exactly the keys kind, contracts and notes, with one contract entry. Notes are 1,408 characters, under the 4,000 limit.\n- HamsterVault's constructor in `src/HamsterVault.sol:209` takes team and game addresses in that order. The manifest passes `$owner` for TEAM and the HamsterHour v2 address for the game.\n- IMD is a constant in source, so it is not a constructor argument. The constructor makes no external calls, so the empty-chain rehearsal in the protected deployment test passes.\n- The protected owner check requires no application contract to be owned by the factory or IMD. TEAM resolves to the launch wallet via `$owner`, satisfying it.\n\n**Build and tests**\n\n```\nRan 4 test suites: 76 tests passed, 0 failed, 0 skipped\n```\n\nThe suite ran with the verifier's fuzz seed. The working tree is clean with no stray files, and only `launch.json` is the deliverable.","treeHash":"56b10fcd63b0acc0047edbb6bcfb31d980e2d4f8","usage":{"cachedInputTokens":242781,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":2477,"runtime":"claude","turns":8,"wallClockMs":48441}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ef31844bb462de78","findings":[{"citation":"resolved","description":"New this round, raised by the independent tester (write_foundry_tests) and reproduced. The brief says the boost may be activated within 365 days of deploy and that otherwise anyone can sweep the 50M into mining. Neither scheduleBoost nor activateBoost checks DEPLOYED_AT + BOOST_DEADLINE, so after day 365 the outcome is a race between TEAM's activateBoost and anyone's sweepBoost; once activateBoost lands, sweepBoost reverts BoostAlreadyActive forever. TEAM is trusted and the sweep is permissionless, so no funds are lost; the activation window is simply not enforced on-chain. Fix preserving the design: in activateBoost (and scheduleBoost) revert when block.timestamp >= DEPLOYED_AT + BOOST_DEADLINE.","line":619,"path":"src/HamsterVault.sol","reproduction":"Ran test_activateAfterDeadline in test/scratch/Round2.t.sol (VaultTest helpers, token bound, squeek = MockERC721): vm.warp(DEPLOYED_AT + 365 days + 1 hours); team scheduleBoost(squeek); warp +12h; team activateBoost(squeek). Expected per brief: activation refused after the 365-day window, leaving only sweepBoost. Actual: boostActive() == true and sweepBoost() reverts BoostAlreadyActive().","severity":"low","snippet":"    function activateBoost(address collection_) external onlyTeam {\n        if (address(collection) != address(0)) revert BoostAlreadyActive();\n        if (boostSwept) revert BoostSweptAlready();\n        if (scheduledCollection == address(0) || scheduledCollection != collection_) revert NothingScheduled();\n        if (block.timestamp < boostReadyAt) revert TimelockNotReady();","title":"activateBoost has no 365-day deadline: TEAM can still activate the boost after day 365 as long as nobody has called sweepBoost"},{"citation":"resolved","description":"Carried from my earlier advisory [low] a405309e, which the author disputed. The behaviour still reproduces: the 100,000 HAMSTER global daily cap and the 10,000 per-NFT cap are taken from globalDayUsed/nftDayUsed in the order claims arrive, so with more than ten fully eligible NFTs on one UTC day the claimants who go first get the boost and equal later claimants get 0. The author's reading is that 'allocated deterministically' is satisfied (same state and same calls give the same result) and that 'no claim order changes anyone's amount' is a mining guarantee, which the base share meets. A pro-rata split needs the day's total eligible demand, i.e. a settlement round, which is a redesign. The README and launch.json notes disclose the claim-order rule. I accept the dispute for admission purposes and record it as a deviation the requester must confirm, not as a blocking defect.","line":770,"path":"src/HamsterVault.sol","reproduction":"Boost active; 11 accounts each register one Squeek more than 24h before window k; each loses 10e18 in k (total 110e18, base 200,000e18 each, wanted boost 100,000e18 clamped to the 10,000e18 per-NFT cap). Before any claim claimableMined(k, acc11) reports boost 10,000e18. After the first ten accounts call claimMined([k], self), globalDayUsed[day] == 100,000e18 and claimableMined(k, acc11) reports boost 0. Expected under an order-independent rule: equal boost for equal positions; actual: the eleventh gets 0.","severity":"info","snippet":"        if (got > 0) globalDayUsed[day] += got;","title":"Boost daily caps are consumed in claim order (documented design choice, author disputes it as a defect; requester should confirm)"},{"citation":"resolved","description":"Carried from my earlier [info] 2f835eae, disputed by the author and accepted: the canonical Squeek address is not in the brief or the task's reads, so it cannot be pinned without guessing. The vault checks only code presence and supportsInterface(0x80ac58cd); TEAM, once, behind a 12h public timelock, chooses the collection. With a collection TEAM controls it could direct up to 100,000 HAMSTER/day of the 50M boost bucket to itself. README and launch.json notes disclose this. Recorded as the single material TEAM power beyond bindToken; if the address becomes known before deploy, pin it as a constant checked in scheduleBoost.","line":627,"path":"src/HamsterVault.sol","reproduction":"TEAM scheduleBoost(X) for any contract X whose supportsInterface(0x80ac58cd) returns true (test/helpers/Mocks.sol MockERC721, as the suite's _activateBoost does); warp 12h; activateBoost(X) succeeds and collection == X. Nothing in the code distinguishes X from the canonical collection.","severity":"info","snippet":"        if (!ok || ret.length < 32 || !abi.decode(ret, (bool))) revert BadCollection();","title":"activateBoost accepts any ERC-165 ERC-721; 'canonical Squeek collection only' rests on TEAM (documented trust assumption)"},{"citation":"resolved","description":"Raised by the independent tester and reproduced. The brief says the sweep moves the 50M into the mining bucket 'the curve is unchanged', and the code follows it literally: miningBucket becomes 800M but emissionAt(loss) = MINING_BUCKET - (MINING_BUCKET >> era) + (inEra >> era) never exceeds 750M, so miningCommitted can never reach the extra 50M and it stays in the vault forever with no path out (no owner or withdrawal, by design). Not a defect against the brief as written; the requester should confirm this is the intended outcome of a sweep, or ask for the curve to scale to the enlarged bucket.","line":645,"path":"src/HamsterVault.sol","reproduction":"Ran test_sweptNeverEmitted in test/scratch/Round2.t.sol: warp to DEPLOYED_AT + 365 days; sweepBoost(); miningBucket() == 800_000_000e18 while emissionAt(type(uint128).max) == 750_000_000e18. Expected if the swept tokens were meant to be mined: a curve total of 800M; actual: 50M permanently unmineable.","severity":"info","snippet":"            miningBucket += boostBucket;\n            boostBucket = 0;","title":"After sweepBoost the 50M moved into miningBucket can never be emitted because emissionAt converges to 750M"}],"hash":"7d652dfb6ffa340c0a4d0d6a81ac3e41d919720a12b0a6f754f4aec0b7e238d4","nodeId":"f9917de5-158d-48aa-b2b4-f6a8848da07b","outcome":"completed","summary":"The findings file is valid and the snippets match the tree. Only `.imd-findings.json` and my scratch tests are new; no project files changed.\n\n**Outcome: all three blocking findings from round 1 are fixed, and no new blocking defect was found.**\n\nSettled from round 1:\n\n- **bindToken 1-wei donation lock (high)**: fixed. The check is now `balanceOf(vault) < 8e26` reverts. My proof copied under test/scratch passes, and the 8e26-1 case still reverts.\n- **SyncRequired outage (medium)**: fixed. Activations aggregate per hourly grid point, the queue holds distinct times, and sync never reverts. My scenario with 65 same-block stakes plus 300 wallets over a day leaves deposits and stakes working with exact total weight. Lazy activation credits from the activation-time accumulator exactly (450/250 split of a 700 IMD stream matched the hand calculation).\n- **Squeek registration never voided (medium)**: fixed. Claims, the new owner's registerSqueek and the permissionless voidSqueek all void on an observed owner change. An unobserved out-and-back transfer still pays, which the contract cannot detect; the README states this limit.\n- **Batch BucketExceeded, stream restart by dust, duplicate list entries (low)**: all fixed and verified by the suite and my own stream test (1 wei left after seven daily 1-wei deposits).\n- **Disputed items**: I accept both disputes. The claim-order boost cap and the unpinned collection are recorded as info with the author's reasoning, so the requester can confirm them without blocking admission.\n\nNew this round:\n\n- **Low**: activateBoost has no 365-day deadline. TEAM can activate after day 365 if nobody swept first, after which the sweep is impossible. Reproduced in my scratch test.\n- **Info**: after a sweep the 50M moved into the mining bucket can never be emitted, since the curve still converges to 750M. This follows the brief literally and needs the requester's confirmation.\n\nAlso verified on mainnet through the local RPC: the game's GENESIS and WINDOW_DURATION equal the vault's constants, every game selector the vault uses resolves, and the IMD address returns symbol IMD. The full suite passes with 135 tests. Coverage answers all 18 entry points plus two invariants.","treeHash":null,"usage":{"cachedInputTokens":1323699,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":26558,"runtime":"claude","turns":14,"wallClockMs":442590}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d981a5cee43d1fd6","findings":[],"hash":"9de9769ae0e69c23c248e7c9c4df80803cd320b2d57fcd538b49f384e33c4623","nodeId":"6ee78cc8-16d9-445c-9f84-8935e1371918","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/… before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6.1-sol","outputTokens":0,"runtime":"codex","turns":4,"wallClockMs":730908}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1da032f42d59f73b","findings":[{"citation":"resolved","description":"Merged from audit_permissions, audit_flow, audit_economics and audit_math (same root cause). bindToken is the only way to set `token` and the buckets; claimMined, stake and withdrawVested revert NotBound until it succeeds. It requires the vault's HAMSTER balance to equal 8e26 exactly. Launch 2 delivers the 8e26 remainder and HAMSTER becomes tradable on its v4 pool before TEAM's separate bindToken transaction. The vault has no owner, sweep or any function that moves HAMSTER out before the bind, so once any third party transfers even 1 wei to the vault address (public since launch 1) the equality can never hold again: bindToken reverts WrongRemainder forever, the 800,000,000 HAMSTER (80% of supply) are stuck, mining/vesting/staking never start, and IMD pulled from the game can never reach stakers holding vested weight. Cost to the griefer: 1 wei of HAMSTER plus gas, and it can front-run TEAM's bind from the mempool. The strict totalSupply() == 1e27 check on line 222 has the same fragility if the launch-2 token can burn before the bind. Fix preserving the brief: revert only if balanceOf(vault) < EXPECTED_REMAINDER; buckets stay 750M/50M from constants and any surplus is untracked, as donations after the bind already are.","line":223,"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\";\n\n/// @dev Minimal ERC-20 standing in for the launch-2 HAMSTER token: 1e27 supply, 8e26 delivered to the vault.\ncontract HamsterToken {\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 Anyone holding 1 wei of HAMSTER can permanently prevent `bindToken`, and with it every claim,\n/// stake and grant withdrawal, by sending that wei to the vault before TEAM binds. There is no path that\n/// moves HAMSTER out of an unbound vault, so the 800,000,000 remainder is stuck forever.\ncontract BindDonationDoSTest is Test {\n    address team = makeAddr(\"team\");\n    address game = makeAddr(\"game\");\n    address attacker = makeAddr(\"attacker\");\n    address pool = makeAddr(\"pool\");\n\n    HamsterVault vault;\n    HamsterToken hamster;\n\n    function setUp() public {\n        vault = new HamsterVault(team, game);\n        hamster = new HamsterToken();\n        // Launch 2: 1B supply, exactly 800M to the vault (remainderTo), 200M to the pool.\n        hamster.mint(address(vault), 8e26);\n        hamster.mint(pool, 2e26);\n        // The attacker buys (here: receives) 1 wei of HAMSTER from the pool.\n        vm.prank(pool);\n        hamster.transfer(attacker, 1);\n    }\n\n    /// @dev Expected: TEAM can bind once the vault holds at least the 800M remainder, so a stray wei\n    /// donated by a third party cannot lock the vault. Actual: the strict `== 8e26` check reverts\n    /// WrongRemainder forever, because no function can move HAMSTER out before the bind.\n    function test_donationBeforeBindDoesNotLockVaultForever() public {\n        // Attacker front-runs TEAM's bind with a 1 wei donation.\n        vm.prank(attacker);\n        hamster.transfer(address(vault), 1);\n        assertEq(hamster.totalSupply(), 1e27);\n        assertEq(hamster.balanceOf(address(vault)), 8e26 + 1);\n\n        vm.prank(team);\n        vault.bindToken(address(hamster)); // reverts WrongRemainder on the current code\n\n        assertEq(address(vault.token()), address(hamster));\n        assertEq(vault.miningBucket(), 750_000_000e18);\n        assertEq(vault.boostBucket(), 50_000_000e18);\n    }\n}","reproduction":"Ran test/scratch/BindDonation.t.sol (the panel's proof, re-run by me): vault = new HamsterVault(team, game); HAMSTER mock mints 8e26 to the vault and 2e26 to a pool; pool sends 1 wei to attacker; attacker hamster.transfer(vault, 1) -> balanceOf(vault) = 8e26+1; team vault.bindToken(hamster). Expected: bind succeeds, miningBucket 750M, boostBucket 50M. Actual: [FAIL: WrongRemainder()]. No function can reduce the vault's balance, so every later bindToken call reverts the same way.","severity":"high","snippet":"        if (IERC20(token_).balanceOf(address(this)) != EXPECTED_REMAINDER) revert WrongRemainder();","title":"bindToken requires balanceOf(vault) == 8e26 exactly: a 1-wei HAMSTER donation before the bind locks the 800M remainder and the whole vault forever"},{"citation":"resolved","description":"Merged from audit_permissions (low), audit_flow (medium) and audit_economics (low). Every stake() appends a queue entry (line 375) even when it merges into an existing warm-up, and _sync processes at most MAX_SYNC = 64 due entries, then reverts SyncRequired if one more is due, rolling back the 64 it processed. stake, requestUnstake, cancelPendingStake, claimRewards, claimMined, withdrawVested, depositRewards, depositDrip and pullFromGame all go through _sync. One wallet (or a contract in one tx) calling stake(1e18) 65 times in one block (65 HAMSTER, recoverable via cancelPendingStake/requestUnstake) arms an outage exactly 24h later, and can re-arm every block. It also happens organically when more than 64 stakes come due between two vault interactions (e.g. a burst of stakes in one block at launch). depositRewards/depositDrip are what launch 2's hook calls on swaps; unless the hook wraps them in try/catch, HAMSTER swaps revert for the duration. Not a fund loss (anyone can call sync(n), cheaper than arming), hence medium. Fix: keep one live queue entry per account (skip the push when the account already has a queued entry for the same activateAt, or dequeue the stale one), and/or let _sync process what fits and advance only up to the first unprocessed activation instead of reverting.","line":497,"path":"src/HamsterVault.sol","reproduction":"Ran test_syncDos in my scratch test (VaultTest helpers, token bound): alice stakes 100e18; attacker stake(1e18) x65 in one block; vm.warp(+1 day); depositRewards(50e18) -> reverts SyncRequired(); depositDrip(50e18) -> reverts SyncRequired(). After vault.sync(64), depositRewards(50e18) succeeds. Expected: deposits and user calls proceed regardless of how many activations are due. Actual: all _sync-gated calls revert until a separate sync transaction.","severity":"medium","snippet":"        if (queueHead < queueTail && _queue[queueHead].activateAt <= block.timestamp) revert SyncRequired();","title":"65 due warm-up activations make every _sync-gated entry point, including the hook's depositRewards/depositDrip, revert SyncRequired until someone calls sync"},{"citation":"resolved","description":"Merged from audit_flow (medium), audit_math (low) and audit_permissions (low, the escrow/third-party-claim variant: same root cause, claim-time ownership snapshot). The brief says a registration 'is void if ownerOf changes'. squeekEligible checks only that registrant[tokenId] == account, the 24h lead, and ownerOf(tokenId) == account at the moment claimMined runs. Nothing voids the registration when the token leaves the registrant, so (a) a registrant who sells or lends the NFT keeps the boost for all windows held by someone else as long as the NFT is back for the claim block (claims batch 48 windows), and (b) a wallet that never holds a Squeek can flash-borrow one to register and again to claim, taking boost (up to the 100k/day global cap, first-come) ahead of real holders. Conversely, because the result is snapshot at claim time and claimMined is permissionless, a third party can claim a registrant's windows while the NFT is temporarily in escrow and permanently fix that boost at 0. Fix: make the void permanent and observable, e.g. delete registrant/registeredAt whenever ownerOf != registrant is observed (in claimMined, registerSqueek, or a permissionless voidSqueek), so re-registration requires a fresh 24h lead; document that only on-chain-observed transfers can be enforced.","line":672,"path":"src/HamsterVault.sol","reproduction":"Ran test_transferOutAndBack in my scratch test: boost active; squeek.mint(alice, 1); alice.registerSqueek(1); k = window starting >= 2 days later; squeek.transfer(1, bob) so bob holds #1 through window k; finalize window k with lossOf(k, alice) = 1e18 (total 1e18); warp 5 days past the window; squeek.transfer(1, alice). claimableMined(k, alice) returns base 20,000e18 and boost 10,000e18. Expected per brief: registration void because ownerOf changed, boost 0. Actual: boost 10,000e18 paid on claim.","severity":"medium","snippet":"        (bool ok, bytes memory ret) = address(collection).staticcall(abi.encodeCall(IERC721.ownerOf, (tokenId)));\n        if (!ok || ret.length < 32) return false;\n        return abi.decode(ret, (address)) == account;","title":"Squeek registration is never voided when ownerOf changes; eligibility is only ownerOf at claim time, so a sold, lent or flash-borrowed NFT still earns the registrant the boost"},{"citation":"resolved","description":"From audit_permissions. The brief asks for boost 'allocated deterministically', in a design where 'no claim order changes anyone's amount'. _allocateBoost consumes globalDayUsed[day] in claim order, so with more than ten fully eligible NFTs in a day, the first claimants (bots claiming the moment a window finalizes) take the whole 100k and later claimants with identical loss and registration get 0. claimableMined shows the boost to everyone until the cap is gone. The launch notes acknowledge 'Boost daily caps are consumed in claim order', so this is a broken guarantee relative to the brief rather than an undocumented behaviour; order independence needs a pro-rata allocation per day (e.g. two-phase: record wanted boost per window, pay pro-rata once the day is closed).","line":682,"path":"src/HamsterVault.sol","reproduction":"Ran test_globalCapFirstCome in my scratch test: boost active; 11 accounts each register one Squeek, then all lose 10e18 in the same window k (total 110e18; base 200,000e18 each, wanted 100,000e18, per-NFT cap 10,000e18). Before any claim claimableMined(k, acc11).boost == 10,000e18. After the first ten accounts claim (globalDayUsed = 100,000e18), claimableMined(k, acc11).boost == 0. Expected: equal accounts get equal boost regardless of order. Actual: the 11th gets 0.","severity":"low","snippet":"        uint256 left = BOOST_GLOBAL_DAILY_CAP - globalDayUsed[day];","title":"Boost global daily cap is first-come: once a UTC day's boost demand exceeds 100,000 HAMSTER, claim order decides who gets boost"},{"citation":"resolved","description":"Merged from audit_flow and audit_math. boostCommitted is increased only after the batch loop (line 274), so every window in one claimMined call clamps against the same pre-batch bucketLeft. Their sum can exceed it, and the post-loop check on line 275 reverts the whole batch instead of clamping the later windows. Claiming one window per call works. Fix: subtract the running boost total from bucketLeft inside the loop (or update boostCommitted per window).","line":683,"path":"src/HamsterVault.sol","reproduction":"Ran test_batchBucketRevert in my scratch test: boost active, alice registers Squeek #1; windows k and k+24 (different UTC days) each with lossOf(alice) = 1e18 = total (base 20,000e18, wanted boost 10,000e18 each); vm.store slot 4 so boostCommitted = 50M - 15,000e18. claimMined([k, k+24], alice) reverts BucketExceeded(). Then claimMined([k]) and claimMined([k+24]) succeed and boostCommitted == boostBucket. Expected: the batch clamps the second window to 5,000e18 and succeeds.","severity":"low","snippet":"        uint256 bucketLeft = boostBucket - boostCommitted;","title":"Multi-window claimMined reverts BucketExceeded near boost-bucket exhaustion because _allocateBoost reads boostCommitted that is only updated after the loop"},{"citation":"resolved","description":"Merged from audit_permissions, audit_economics and audit_math. depositRewards is permissionless with no minimum, and _addToStream merges the deposit and sets end = now + 7 days. Each deposit re-spreads the leftover over a fresh 7 days, so 1 wei a day makes the stream decay geometrically (6/7 per day) instead of finishing in 7 days. Stakers who leave after the nominal 7 days forfeit the tail. Nothing is lost. Fix: extend the end only in proportion to the new amount (weighted end), or keep a per-second rate that deposits add to.","line":485,"path":"src/HamsterVault.sol","reproduction":"Ran test_streamRestart in my scratch test: alice stakes 100e18 and activates; depositRewards(7000e18) at T; depositRewards(1) at T+1d ... T+7d. At T+7d stream.remaining = 2,379e18 and stream.end = T+14d. Expected (7-day stream): remaining 0 at T+7d.","severity":"low","snippet":"    function _addToStream(Stream storage s, uint256 amount, uint64 duration) private returns (uint64 end) {\n        s.remaining += amount;\n        end = uint64(block.timestamp) + duration;\n        s.end = end;","title":"Any depositRewards (even 1 wei) restarts the 7-day stream for the whole leftover, so anyone can stretch stakers' payouts indefinitely"},{"citation":"resolved","description":"From audit_math. The duplicate guard checks only registrant[tokenId] == msg.sender, not the caller's _squeeks list. After A registers, B re-registers, and A registers again, A's list holds the id twice. Boost cannot be double-counted (nftDayUsed is per id), but each copy uses one of 20 slots and must be removed with a separate unregisterSqueek call. Also, stale ids stay in a former registrant's list until they unregister them manually. No funds impact.","line":635,"path":"src/HamsterVault.sol","reproduction":"Ran test_duplicateList in my scratch test: boost active; alice owns #1 and registers it; transfer #1 to bob, bob.registerSqueek(1); transfer #1 back, alice.registerSqueek(1). squeeksOf(alice).length == 2 (expected 1).","severity":"low","snippet":"        if (registrant[tokenId] == msg.sender) revert AlreadyRegistered();","title":"registerSqueek pushes a duplicate tokenId into the caller's list when the token was registered by someone else in between, wasting MAX_SQUEEKS slots"},{"citation":"resolved","description":"Merged from audit_permissions, audit_flow and audit_economics. The brief restricts the boost to the canonical Squeek collection on mainnet. The code checks only that the address has code and supportsInterface(0x80ac58cd) is true. The README and launch.json notes disclose this. With a freely mintable collection, TEAM could direct up to 100,000 HAMSTER/day of the 50M bucket to itself. The 12h timelock makes the choice visible. If the canonical address is known before deploy, pin it as a constant checked in scheduleBoost.","line":602,"path":"src/HamsterVault.sol","reproduction":"TEAM scheduleBoost(X) for any contract X whose supportsInterface(0x80ac58cd) returns true (e.g. test/helpers/Mocks.sol MockERC721, as the suite's _activateBoost does); warp 12h; activateBoost(X) succeeds. Nothing distinguishes X from the canonical collection.","severity":"info","snippet":"        if (!ok || ret.length < 32 || !abi.decode(ret, (bool))) revert BadCollection();","title":"activateBoost accepts any ERC-165 ERC-721; 'canonical Squeek collection only' rests on TEAM (documented trust assumption)"}],"hash":"aac45b244899ca3e7350b34dd726377e1a3df3e22f79ee9be05eddc557264c16","nodeId":"f9917de5-158d-48aa-b2b4-f6a8848da07b","outcome":"completed","summary":"I've reviewed the vault and written the result to `.imd-findings.json`: 8 findings and 19 coverage rows (every one of the 17 entry points, plus two invariants). The specialists' 20 findings merged down to 8, and all 8 reproduced. I re-ran the bind proof and wrote my own scratch tests for six of the others; the collection finding is shown by the existing suite's own setup. I found nothing new beyond what the panel reported.\n\nI also checked the vault against the real HamsterHour v2 source on GitHub. The functions it calls exist there with matching signatures, the genesis time and window length match, and the game's `withdraw()` pays the vault in IMD. The pre-vault backlog is moved into the vault's balance when TEAM completes the staking-recipient change on the game, so the first `pullFromGame` does pick it up.\n\n| # | Severity | Finding |\n|---|---|---|\n| 1 | **high** | `bindToken` needs the vault to hold exactly 8e26 HAMSTER (`src/HamsterVault.sol:223`). Anyone can send it 1 wei before TEAM binds, and nothing can move HAMSTER out of an unbound vault. Every later bind then fails, the 800M stay locked, and mining and staking never start. Four specialists reported this; the proof fails with `WrongRemainder()` and is attached. |\n| 2 | medium | Once more than 64 warm-up activations are due at once, `_sync` reverts with `SyncRequired` (`:497`). An attacker can arrange that with 65 stakes of 1 HAMSTER, and a burst of real stakes can cause it too. Until someone calls `sync`, nearly every user call fails, including the hook's `depositRewards` and `depositDrip`. Unless launch 2's hook catches that error, swaps would fail too. Confirmed. |\n| 3 | medium | A Squeek registration is never cancelled when the NFT changes owner; ownership is only checked at claim time (`:672`). A sold, lent or flash-borrowed NFT still earns its registrant the boost. The flip side: anyone can claim for a holder while their NFT is in escrow and lock that boost at 0. Three specialist reports merged; confirmed. |\n| 4 | low | The 100,000 HAMSTER daily boost cap goes to whoever claims first (`:682`). Confirmed: the 11th of 11 equal accounts gets 0. |\n| 5 | low | Within one multi-window claim, each window checks the boost bucket against the amount left before the claim started (`:683`). Near the end of the bucket the whole claim reverts `BucketExceeded`, though claiming one window at a time works. Confirmed. |\n| 6 | low | A 1-wei `depositRewards` restarts the 7-day stream for everything left in it (`:485`). Confirmed: one wei a day for a week leaves 2,379 of 7,000 IMD unpaid when the stream should have finished. |\n| 7 | low | Re-registering a Squeek can list the same NFT twice for one wallet, using up its 20 registration slots (`:635`). Confirmed. |\n| 8 | info | `activateBoost` accepts any ERC-721, not only the real Squeek collection (`:602`). This is a trust in TEAM that the README and launch notes disclose, not a bug. |\n\nOnly `scheduleBoost`, `sweepBoost`, `sync`, `unregisterSqueek` and `withdrawUnstaked` passed without a finding. The other entry points are each tied to the finding that affects them, mostly #2. I deleted my scratch tests afterwards and changed no files in the repository.","treeHash":null,"usage":{"cachedInputTokens":1172316,"inputTokens":28,"model":"claude-opus-5-5","outputTokens":26832,"runtime":"claude","turns":15,"wallClockMs":309200}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fcb71e606c933181","findings":[{"citation":"resolved","description":"bindToken is the only way the vault ever becomes usable (claimMined, stake and withdrawVested all revert NotBound), and it demands balanceOf(vault) == 8e26 exactly. The vault has no owner, no withdrawal and no pre-bind transfer path, so once its HAMSTER balance differs from 8e26 by any amount the condition can never be restored. Launch 2 delivers the 800M remainder to the vault in the token's own deployment transaction, after which HAMSTER trades on the pool; TEAM calls bindToken in a later transaction. Anyone who buys a dust amount of HAMSTER and transfers 1 wei to the vault address (public since launch 1) in between makes every bindToken call revert WrongRemainder permanently. Cost to the griefer: 1 wei of HAMSTER plus gas. Result: 800,000,000 HAMSTER (80% of supply) are locked in a dead contract, no mining, vesting or staking ever starts, and the game's staking recipient (the vault) accumulates IMD that can be pulled but never distributed. The same strict equality on totalSupply() == 1e27 (line 222) is also a permanent brick if the launch-2 token burns any amount (e.g. a burned portion of the anti-snipe tax) before the bind; the token's behaviour is not visible here so that part is a note. Minimal fix that keeps the brief's intent (prove the full remainder arrived): require balanceOf(vault) >= EXPECTED_REMAINDER and set the buckets from the constants as now; any excess stays untracked dust (trackedHamster already tolerates donations after the bind).","line":223,"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\";\n\ncontract DonationToken {\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\ncontract GameStub {\n    function nextFinalizeWindow() external pure returns (uint256) {\n        return 0;\n    }\n}\n\n/// @notice A 1-wei HAMSTER transfer to the vault before `bindToken` makes the bind revert forever:\n/// the vault has no path to move tokens out before it is bound, so the 800M remainder is locked.\ncontract BindDonationTest is Test {\n    address team = makeAddr(\"team\");\n    address griefer = makeAddr(\"griefer\");\n    HamsterVault vault;\n    DonationToken hamster;\n\n    function setUp() public {\n        vault = new HamsterVault(team, address(new GameStub()));\n        hamster = new DonationToken();\n        // Launch 2: 1B supply, 800M delivered to the vault as remainderTo, 200M to the pool.\n        hamster.mint(address(vault), 8e26);\n        hamster.mint(address(0xBEEF), 2e26 - 1);\n        hamster.mint(griefer, 1);\n    }\n\n    function test_oneWeiDonationBeforeBindLocksVaultForever() public {\n        // The griefer bought 1 wei of HAMSTER on the pool and sends it to the vault before TEAM binds.\n        vm.prank(griefer);\n        hamster.transfer(address(vault), 1);\n        assertEq(hamster.totalSupply(), 1e27);\n        assertEq(hamster.balanceOf(address(vault)), 8e26 + 1);\n\n        // Expected: TEAM can still bind (the full remainder is present; the extra wei is untracked dust).\n        // Actual on current code: WrongRemainder, and there is no call that can ever make it pass.\n        vm.prank(team);\n        vault.bindToken(address(hamster));\n        assertEq(address(vault.token()), address(hamster));\n        assertEq(vault.miningBucket(), 750_000_000e18);\n        assertEq(vault.boostBucket(), 50_000_000e18);\n    }\n}","reproduction":"State: HamsterVault deployed; HAMSTER token with totalSupply 1e27 minted 8e26 to the vault and 2e26 elsewhere (launch 2). Call sequence: (1) any address holding 1 wei HAMSTER calls hamster.transfer(vault, 1) -> balanceOf(vault) = 8e26 + 1; (2) TEAM calls vault.bindToken(hamster). Expected: the bind succeeds (the full remainder is present) and miningBucket = 750M, boostBucket = 50M. Actual: revert WrongRemainder(), and no later call by anyone can change the vault's balance back, so step (2) reverts forever. Run: forge test --match-path test/scratch/BindDonation.t.sol (fails with WrongRemainder on current code).","severity":"high","snippet":"        if (IERC20(token_).balanceOf(address(this)) != EXPECTED_REMAINDER) revert WrongRemainder();","title":"bindToken requires an exact 8e26 balance: a 1-wei HAMSTER donation before the bind locks the 800M remainder forever"},{"citation":"resolved","description":"Every stake pushes an Activation onto a single global FIFO queue and _sync() reverts when more than MAX_SYNC (64) entries are due. An account may call stake(MIN_STAKE) any number of times in one block: each call appends an entry with the same activateAt even though the pending amount is merged. Exactly 24 hours later all of them are due at once, so claimRewards, claimMined, withdrawVested, requestUnstake, cancelPendingStake, depositRewards, depositDrip and pullFromGame all revert SyncRequired until someone spends an extra transaction on sync(64). The griefer needs 65 HAMSTER (returned after cancelPendingStake + 24h) and about 10M gas, and can schedule the outage to land on a chosen second, e.g. the block in which the hook deposits swap fees. Because launch 2's hook is expected to call depositRewards/depositDrip from inside the pool manager callback, a revert there bubbles into the swap unless the hook wraps the call; this is the flow-gap seam the hook author must know about. Impact is bounded (one extra sync call per 64 entries, no funds lost), hence low. Mitigations that keep the design: do not push a new queue entry when the caller already has a pending entry at the same activateAt (merge instead), or let _sync drain the queue in bounded gas without reverting (process what fits and advance lastSync only up to the first unprocessed activation).","line":497,"path":"src/HamsterVault.sol","reproduction":"State: token bound, alice staked 100e18 and 700e18 IMD streaming. Calls: attacker calls stake(1e18) 65 times in the same block; vm.warp(+24h); alice.claimRewards() -> SyncRequired; depositRewards(1e18) -> SyncRequired; depositDrip(1e18) -> SyncRequired; claimMined([0], bob) -> SyncRequired. After vault.sync(64) the same calls succeed. Test: test_sixtyFiveOneTokenStakesBlockEveryEntryPointUntilSync in test/scratch/Repro.t.sol (passes, demonstrating the revert).","severity":"low","snippet":"        if (queueHead < queueTail && _queue[queueHead].activateAt <= block.timestamp) revert SyncRequired();","title":"65 minimum stakes in one block make every synced entry point revert SyncRequired 24h later, including the hook's depositRewards/depositDrip"},{"citation":"resolved","description":"depositRewards is permissionless with no minimum, and _addToStream merges the deposit into stream.remaining and moves stream.end to now + 7 days. The leftover of an existing stream is therefore re-spread over a fresh 7 days on every deposit, however small. A griefer depositing 1 wei of IMD once a day turns a 7-day stream into a geometric tail: each day pays 1/7 of what is left, so after 7 days only 1-(6/7)^7 = 66% of the hook's swap share has reached stakers instead of 100%, after 30 days 99%. The same applies to every non-first pullFromGame (whose timing is also permissionless). Nothing is lost, but the brief's 7-day schedule is not enforced and stakers who unstake on time receive less than their pro-rata share of the deposits that were made while they were staked. Fix that preserves merging: keep the existing end when the new deposit is below some fraction of remaining, or stream each deposit at its own rate (remaining/duration per second as a rate accumulator) so later deposits add rate instead of extending the end.","line":485,"path":"src/HamsterVault.sol","reproduction":"State: alice staked 100e18 and activated; depositRewards(7000e18) at T. Calls: a griefer with 7 wei IMD calls depositRewards(1) at T+1d, T+2d, ... T+7d. Expected (7-day stream): pendingRewards(alice) at T+7d = 7000e18. Actual: 4620.58e18 paid, 2379.4e18 still in stream.remaining with stream.end = T+14d. Test: test_oneWeiDailyDepositsSlowStream in test/scratch/Repro.t.sol.","severity":"low","snippet":"    function _addToStream(Stream storage s, uint256 amount, uint64 duration) private returns (uint64 end) {\n        s.remaining += amount;\n        end = uint64(block.timestamp) + duration;\n        s.end = end;\n    }","title":"Any 1-wei depositRewards restarts the 7-day stream for the whole leftover, so a griefer can slow staker payouts indefinitely"},{"citation":"resolved","description":"The brief restricts the boost to the canonical Squeek NFT collection on Ethereum mainnet. The code checks only that the scheduled address has code and answers true to supportsInterface(0x80ac58cd); the README says the canonical address was not available to the builder. This is a trust assumption on TEAM's one-shot power rather than a bypass: with a collection TEAM or anyone can mint freely, the per-NFT 10,000/day cap stops binding and the 50M boost bucket goes, 100,000 per day, to whoever claims first with the most registered tokens, instead of to Squeek holders. Recommended: once the canonical address is known, hard-code it (or its code hash) as the only accepted collection, which the 12-hour timelock already gives the community time to inspect.","line":602,"path":"src/HamsterVault.sol","reproduction":"State: any contract C whose supportsInterface(0x80ac58cd) returns true and whose ownerOf is mintable by its deployer. Calls: TEAM scheduleBoost(C); +12h; TEAM activateBoost(C) succeeds. Then a holder of 20 freshly minted C tokens registers them, waits 24h, and each claimMined window with base b yields boost min(b/2, 100,000e18 - globalDayUsed) across its 20 tokens. Expected per brief: only the canonical Squeek collection is accepted.","severity":"info","snippet":"        if (!ok || ret.length < 32 || !abi.decode(ret, (bool))) revert BadCollection();","title":"activateBoost accepts any ERC-165 ERC-721, so the 'canonical Squeek collection only' rule rests entirely on TEAM"}],"hash":"bbdfbe754d11530509114ff9bb0897240bb29811c865b702cc55ccaddff68fa6","nodeId":"b8fb6976-b9e2-437b-86eb-b15f8f2d2a2a","outcome":"completed","summary":"The findings file is written and validated. Final recap:\n\n**Scope covered.** Economic Security, Invariant and Flow Gap passes over `src/HamsterVault.sol`, checked against the real HamsterHour v2 source (fetched from the named repository). Settlement there is per window, so `lossOf` is final once a window is finalized and the permissionless `claimMined` cannot burn a later-settled share. The curve, telescoping budgets, bucket conservation, zero-weight pause, warm-up activation ordering, drip math and both tracked-balance invariants all held under tracing and scratch tests. All 17 entry points have coverage rows, plus three invariant rows.\n\n**Findings written to `.imd-findings.json`:**\n\n1. **High, with proof test.** `bindToken` demands exactly 8e26 HAMSTER. Anyone who sends 1 wei of HAMSTER to the vault between the launch-2 token deployment and TEAM's bind makes the bind revert forever, and the vault has no path to move tokens out. 800M HAMSTER would be locked in a dead contract. The proof at `test/scratch/BindDonation.t.sol` fails on current code with `WrongRemainder`. The same strict equality on `totalSupply` is a brick if the token ever burns before the bind, which I could not verify and noted as such.\n2. **Low.** Sixty-five minimum stakes from one account in one block create 65 queue entries due at the same second, so every synced entry point, including the hook's `depositRewards` and `depositDrip`, reverts `SyncRequired` until an extra `sync` call. Reproduced in `test/scratch/Repro.t.sol`.\n3. **Low.** A 1-wei `depositRewards` restarts the 7-day stream for the whole leftover. Daily 1-wei deposits leave only 66% of a deposit paid after 7 days instead of 100%. Reproduced with numbers in the same scratch file.\n4. **Info.** `activateBoost` accepts any ERC-165 ERC-721, so the brief's \"canonical Squeek collection only\" rule is a TEAM trust assumption rather than an on-chain guarantee.\n\n**Not reached.** The launch-2 hook's own handling of a reverting deposit call, and the real HAMSTER token's transfer and burn behaviour, which are outside this tree and affect findings 1 and 2. The scratch tests under `test/scratch/` are review material and are not part of the submission.","treeHash":null,"usage":{"cachedInputTokens":1229643,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":40425,"runtime":"claude","turns":26,"wallClockMs":597167}},{"artifacts":[],"attempt":2,"bundleHash":"c23748f4b7977bd21ef0548eefd231aaecff5ff95aeaa693c537ca8df531e8a2","device":"f96a0c89c039c22a","findings":[],"hash":"bf07c42cbae571f4e1fb62fef4968fabbf124e47f4b7cf62257a74c3c9b756b8","nodeId":"6ee78cc8-16d9-445c-9f84-8935e1371918","outcome":"completed","summary":"I added an invariant test suite at `test/invariant/VaultInvariant.t.sol`. The full `forge test` passes with the verifier's seed: 67 tests across 5 suites, 0 failures.\n\n**What was already there.** Earlier contributors' tests cover most of the brief, and they all passed when I started. That includes retroactive claims, halving crossings, claim order, bind checks, warm-up, unstake timing, drip math, no first-staker windfall, boost voiding, the daily caps and the 365-day sweep. What was missing was the required invariant testing over random call sequences.\n\n**What I added.** A test contract drives the vault in random order with four users and bounded amounts. Its actions are:\n- stake, unstake, cancel a pending stake, and withdraw\n- claim IMD rewards, and claim mined HAMSTER from random windows\n- deposit to the 7-day stream and to the drip\n- send the game's staker share and pull it into the vault\n- finalize new game windows, including some with leftover loss\n- withdraw vested grants\n- move time forward\n\nAfter every call, five properties must hold:\n1. **HAMSTER solvency:** the vault's HAMSTER balance exactly equals what it says it owes (unclaimed buckets plus grants plus stakes plus pending unstakes).\n2. **Bucket caps:** committed emissions never exceed the mining or boost bucket, and the two buckets always total 800M.\n3. **IMD conservation:** every IMD paid in is either still in the vault, still in the game waiting to be pulled, or has been paid to a user. Claimed rewards never exceed rewards earned.\n4. **Pending rewards are covered:** the sum of all users' pending rewards never exceeds the vault's IMD balance.\n5. **Fixed supply:** HAMSTER is never created or destroyed through the vault.\n\nAll five held at 64 runs × 40 calls each (set in the file, since `foundry.toml` is off limits). I found no defects, so I did not write `.imd-findings.json`.\n\n**Two things to know:**\n- **Not committed.** `git commit` failed because this machine has no git name or email set. The new file is in the working tree, uncommitted.\n- **\"Cancel pending stake\" reverted in 5 of 213 calls,** even though the test only calls it when a pending stake exists. I didn't look into why. My guess is the vault's backlog guard (`SyncRequired`), but I haven't confirmed it, and these reverts don't count as failures under the test's settings.","treeHash":"8d8acc415446b6fb8041cda652c583f9cade159e","usage":{"cachedInputTokens":311148,"inputTokens":16,"model":"claude-opus-5-5","outputTokens":5646,"runtime":"claude","turns":8,"wallClockMs":73636}},{"artifacts":[],"attempt":1,"bundleHash":"a80056531965e3b2c9b3dd44982940f414fdb745cf35a5e0ec39e72fd519fcec","device":"9967c7aa2bba256a","findings":[],"hash":"e79e4bc12a2d8050c7f19b9f5321841cb4050e3fc3320eef5eec3e9a6aa976fa","nodeId":"dc184581-38e1-4c78-8753-cf804c689963","outcome":"completed","summary":"Created `launch.json` for `HamsterVault` with `$owner` as TEAM and the supplied HamsterHour v2 address, matching the accepted constructor ABI.\n\nOnly `launch.json` changed. `forge build` succeeded; all 66 tests passed with the required fuzz seed.","treeHash":"633d3282447d38b60df0fd1632af916c1cd246c4","usage":{"cachedInputTokens":191232,"inputTokens":29495,"model":"gpt-6.1-sol","outputTokens":1650,"runtime":"codex","turns":3,"wallClockMs":47893}}],"verification":[{"checks":[{"durationMs":3604,"exitCode":0,"name":"build","output":"Compiling 33 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.49s\nCompiler run successful!\n","passed":true},{"durationMs":838,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_constructorRejectsZero() (gas: 3903)\n[PASS] test_deployWithRealParamsOnEmptyChain() (gas: 9251723)\n[PASS] test_noOwnerPowersBeyondTwo() (gas: 71176)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 1.10ms (816.98µs CPU time)\n\nRan 20 tests for test/Boost.t.sol:BoostTest\n[PASS] test_activateNotTeamReverts() (gas: 34690)\n[PASS] test_activateRejectsNonErc721() (gas: 190662)\n[PASS] test_activateRequiresScheduleAndTimelock() (gas: 268291)\n[PASS] test_batchClampsAtBoostBucketEnd() (gas: 15045054)\n[PASS] test_boostBucketConservation() (gas: 13139431)\n[PASS] test_boostEarnsOnlyAfterLead() (gas: 13358083)\n[PASS] test_claimObservingTransferVoidsPermanently() (gas: 13278036)\n[PASS] test_globalDailyCap() (gas: 17619361)\n[PASS] test_newOwnerCanRegisterAfterTransfer() (gas: 449855)\n[PASS] test_perNftDailyCap() (gas: 13839630)\n[PASS] test_reRegisterDoesNotDuplicateList() (gas: 526067)\n[PASS] test_registerRequiresActiveAndOwnership() (gas: 486833)\n[PASS] test_sweepBeforeBindThenBind() (gas: 725075)\n[PASS] test_sweepBlockedOnceActive() (gas: 171190)\n[PASS] test_sweepMovesBucketOnce() (gas: 199536)\n[PASS] test_sweepTooEarly() (gas: 39863)\n[PASS] test_transferOutAndBackStaysVoid() (gas: 13174661)\n[PASS] test_transferVoidsRegistration() (gas: 13248750)\n[PASS] test_twoNftsDoubleTheCap() (gas: 13310160)\n[PASS] test_voidSqueekChecks() (gas: 428217)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 62.21ms (100.17ms CPU time)\n\nRan 56 tests for test/VaultEdges.t.sol:VaultEdgesTest\n[PASS] testFuzz_budgetTelescopes(uint256,uint256,uint256) (runs: 512, μ: 456861, ~: 460006)\nLogs:\n  Bound result 17367\n  Bound result 99999999999999999999063\n  Bound result 60000\n\n[PASS] testFuzz_claimOrderIndependentAcrossThreeAccounts(uint256,uint256,uint256,uint256,uint8) (runs: 256, μ: 1960339, ~: 1988520)\nLogs:\n  Bound result 25120711815212230179252\n  Bound result 17641941528373238562833\n  Bound result 24090782666581\n  Bound result 6395694864782588482479\n\n[PASS] testFuzz_dripSplitSyncMatchesSingle(uint256,uint32,uint32) (runs: 256, μ: 852364, ~: 852710)\nLogs:\n  Bound result 999999999000000000000001485\n  Bound result 43690\n  Bound result 8447\n\n[PASS] testFuzz_noFirstStakerWindfall(uint256,uint256,uint256) (runs: 256, μ: 921673, ~: 923406)\nLogs:\n  Bound result 99999993000000000000001485\n  Bound result 4707694\n  Bound result 8447\n\n[PASS] testFuzz_vestingIsLinearAndMonotone(uint256,uint256,uint256) (runs: 256, μ: 657990, ~: 661523)\nLogs:\n  Bound result 4825730527471418593616\n  Bound result 712723\n  Bound result 864000\n\n[PASS] test_activateOneSecondEarlyReverts() (gas: 223046)\n[PASS] test_backlogStreamsOverThirtyDaysOfWeight() (gas: 907409)\n[PASS] test_backlogThenLaterPullsJoinStream() (gas: 612869)\n[PASS] test_batchOfExactly48Accepted() (gas: 9322136)\n[PASS] test_batchWithOneUnfinalizedWindowRevertsWhole() (gas: 261389)\n[PASS] test_bindRejectsNonContractToken() (gas: 35954)\n[PASS] test_bindRejectsRemainderOneWeiShort() (gas: 617366)\n[PASS] test_bindRejectsSupplyOffByOne() (gas: 608571)\n[PASS] test_bindRejectsZeroToken() (gas: 33173)\n[PASS] test_cancelPendingWithNothingPendingReverts() (gas: 532494)\n[PASS] test_cancelledPendingEarnsNothing() (gas: 852952)\n[PASS] test_claimRewardsTwiceSecondReverts() (gas: 841354)\n[PASS] test_claimZeroAccountReverts() (gas: 183734)\n[PASS] test_depositRewardsWithoutAllowanceReverts() (gas: 149267)\n[PASS] test_dripOneDayIsOnePercent() (gas: 740764)\n[PASS] test_dripPausesWithoutWeight() (gas: 264874)\n[PASS] test_duplicateWindowInBatchRevertsAtomically() (gas: 565226)\n[PASS] test_emptyBatchReverts() (gas: 188982)\n[PASS] test_emptyFinalizedWindowGivesNothing() (gas: 146515)\n[PASS] test_failedBindCanBeRetried() (gas: 731836)\n[PASS] test_grantWeightAndStakeShareProRata() (gas: 1233740)\n[PASS] test_maxTwentyRegistrationsAndUnregisterFreesSlot() (gas: 4639643)\n[PASS] test_nftDailyCapResetsOnNextUtcDayByWindowStart() (gas: 18750376)\n[PASS] test_pullFromGameBeforeRecipientReverts() (gas: 180029)\n[PASS] test_registerBurnedTokenReverts() (gas: 175804)\n[PASS] test_registrationLeadExactBoundary() (gas: 17145059)\n[PASS] test_requestUnstakeZeroAndPendingRevert() (gas: 378666)\n[PASS] test_rescheduleRestartsTimelock() (gas: 157370)\n[PASS] test_scheduleBoostChecks() (gas: 59912)\n[PASS] test_soldTokensFreeSlotsForNewOnes() (gas: 4495217)\n[PASS] test_stakeExactlyMinimumAndOneWeiBelow() (gas: 365353)\n[PASS] test_stakeWithoutAllowanceReverts() (gas: 128498)\n[PASS] test_sweepBoundaryAndOnce() (gas: 216206)\n[PASS] test_sweepCancelsPendingSchedule() (gas: 163732)\n[PASS] test_teamCannotBindASecondTokenEvenIfValid() (gas: 612954)\n[PASS] test_thirdPartyClaimCannotRedirectButStartsVestingClock() (gas: 474448)\n[PASS] test_unboundVaultRefusesStakeAndVesting() (gas: 98765)\n[PASS] test_unregisterAfterOwnershipChangeKeepsNewRegistrant() (gas: 464792)\n[PASS] test_unstakeReleaseBoundaryExactSecond() (gas: 689193)\n[PASS] test_unstakedTokensEarnNothingDuringDelay() (gas: 1293804)\n[PASS] test_vestingBoundaries() (gas: 680943)\n[PASS] test_viewsReportStreamAndDripState() (gas: 806438)\n[PASS] test_voidSqueekOnBurnedToken() (gas: 435027)\n[PASS] test_warmupBoundaryExactSecond() (gas: 481491)\n[PASS] test_windowCrossingTwoHalvings() (gas: 668909)\n[PASS] test_windowsBeforeActivationNeverBoosted() (gas: 816265)\n[PASS] test_withdrawVestedAtGrantStartReverts() (gas: 474357)\n[PASS] test_withdrawVestedCapsAt64GrantsPerCall() (gas: 23623777)\n[PASS] test_withdrawVestedTwiceInSameSecondReverts() (gas: 593484)\n[PASS] test_withdrawVestedZeroMaxReverts() (gas: 469128)\n[PASS] test_zeroLossWindowInBatchIsMarkedButHarmless() (gas: 880730)\nSuite result: ok. 56 passed; 0 failed; 0 skipped; finished in 66.06ms (315.86ms CPU time)\n\nRan 28 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_imdConservation(uint256,uint256,uint256,uint256,uint32) (runs: 256, μ: 1371900, ~: 1143729)\nLogs:\n  Bound result 14999999999999999985\n  Bound result 999999000000000000259201\n  Bound result 8\n  Bound result 7011\n\n[PASS] test_activationIsExactWithoutPoke() (gas: 1020476)\n[PASS] test_backlogStreamsOver30Days() (gas: 1173130)\n[PASS] test_bucketsNeverCountAsWeight() (gas: 281790)\n[PASS] test_cancelPendingStake() (gas: 667357)\n[PASS] test_cancelThenRestakeKeepsBucketsConsistent() (gas: 760730)\n[PASS] test_claimRewardsNothingReverts() (gas: 58883)\n[PASS] test_depositRewardsMergesLeftover() (gas: 855340)\n[PASS] test_depositZeroReverts() (gas: 62165)\n[PASS] test_dripFactorAccuracy() (gas: 7885)\n[PASS] test_dripNotPokeable() (gas: 4124858)\n[PASS] test_dripOnePercentPerDay() (gas: 967181)\n[PASS] test_dustDepositsCannotStretchStream() (gas: 2102495)\n[PASS] test_gapBetweenStakersCarriesRewards() (gas: 1299237)\n[PASS] test_grantsAreNotTransferableOrStakeable() (gas: 475071)\n[PASS] test_lazyActivationCreditsFromActivationTime() (gas: 781093)\n[PASS] test_manyDueActivationsNeverBlockDeposits() (gas: 52648456)\n[PASS] test_multipleGrantsCursor() (gas: 1059828)\n[PASS] test_proRataByWeight() (gas: 1252237)\n[PASS] test_restakeDuringWarmupRestarts() (gas: 768140)\n[PASS] test_stakeBelowMinimumReverts() (gas: 139815)\n[PASS] test_unstakeMoreThanActiveReverts() (gas: 350587)\n[PASS] test_unstakeQueueOrder() (gas: 824956)\n[PASS] test_unstakeStopsWeightAndReleasesAfter24h() (gas: 1158511)\n[PASS] test_vestingCountsAsWeightAndVestsLinearly() (gas: 1250578)\n[PASS] test_warmupEndsOnNextWindowBoundary() (gas: 514944)\n[PASS] test_warmupThenWeight() (gas: 840940)\n[PASS] test_zeroStakerPeriodWaitsNoWindfall() (gas: 940247)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 66.06ms (106.46ms CPU time)\n\nRan 25 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_conservationOfMiningBucket(uint256[8],uint8) (runs: 256, μ: 3553327, ~: 3242493)\nLogs:\n  Bound result 34218004887090175677608\n  Bound result 1302122150100681\n  Bound result 2816666343476623043554\n  Bound result 29206677242238528209641\n  Bound result 21779380742503932799988\n  Bound result 1812642129568\n  Bound result 31756190291244720059425\n  Bound result 29674230579332778217158\n\n[PASS] testFuzz_curveMonotoneAndBounded(uint256,uint256) (runs: 256, μ: 22844, ~: 22327)\nLogs:\n  Bound result 8938\n  Bound result 31536000\n\n[PASS] test_bindAcceptsDonatedSurplus() (gas: 747314)\n[PASS] test_bindNotTeamReverts() (gas: 35322)\n[PASS] test_bindSetsBuckets() (gas: 56854)\n[PASS] test_bindTwiceReverts() (gas: 36813)\n[PASS] test_bindWrongRemainderReverts() (gas: 611585)\n[PASS] test_bindWrongSupplyReverts() (gas: 558958)\n[PASS] test_claimAlwaysPaysAccountNotCaller() (gas: 461822)\n[PASS] test_claimBatchTooLargeReverts() (gas: 50984)\n[PASS] test_claimBeforeBindReverts() (gas: 182587)\n[PASS] test_claimNothingReverts() (gas: 224744)\n[PASS] test_claimOrderIndependence() (gas: 2209449)\n[PASS] test_claimTwiceReverts() (gas: 472722)\n[PASS] test_claimUnfinalizedReverts() (gas: 189288)\n[PASS] test_claimableViewMatchesClaim() (gas: 515902)\n[PASS] test_curveConvergesToBucket() (gas: 18124)\n[PASS] test_curveEra0Rate() (gas: 17919)\n[PASS] test_curveHalves() (gas: 27072)\n[PASS] test_dustTracked() (gas: 1688525)\n[PASS] test_fullCurveNeverExceedsBucket() (gas: 4037067)\n[PASS] test_halvingCrossingWithinWindow() (gas: 378777)\n[PASS] test_hedgedTotalDoesNotOverDistribute() (gas: 693705)\n[PASS] test_rateView() (gas: 197538)\n[PASS] test_retroactiveClaimFromWindowZero() (gas: 1383361)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 108.71ms (154.27ms CPU time)\n\nRan 1 test for test/invariant/VaultInvariant.t.sol:VaultInvariantTest\n[PASS]\nVaultInvariantTest invariants:\n[PASS] invariant_bucketsNeverExceeded\n[PASS] invariant_hamsterSolvent\n[PASS] invariant_hamsterSupplyFixed\n[PASS] invariant_imdSolventAndConserved\n[PASS] invariant_pendingRewardsCovered\n VaultInvariantTest invariants (runs: 64, calls: 2560, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | cancelPending    | 213   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claimMined       | 228   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claimRewards     | 200   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositDrip      | 209   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositRewards   | 187   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | finalize         | 218   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | gameFees         | 200   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | requestUnstake   | 204   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | stake            | 247   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | warp             | 219   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawUnstaked | 225   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawVested   | 210   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 187607\n  Bound result 50000000000000000000\n  Bound result 772478236171505666037704\n  Bound result 999999000000000000000027\n  Bound result 2521970245\n  Bound result 249600\n  Bound result 959\n  Bound result 9900000000000000000000\n  Bound result 2740\n  Bound result 1\n  Bound result 209504\n  Bound result 8768\n  Bound result 10000000000000000000000\n  Bound result 5375048135497692576\n  Bound result 3599\n  Bound result 999999883676675127\n  Bound result 999999999999999624991\n  Bound result 5054\n  Bound result 518400\n  Bound result 20959\n  Bound result 15177\n  Bound result 2826\n  Bound result 350000000000000000000\n  Bound result 435\n  Bound result 57849\n  Bound result 4291\n  Bound result 3\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 546.61ms (539.91ms CPU time)\n\nRan 1 test for test/invariant/BoostInvariant.t.sol:BoostInvariantTest\n[PASS]\nBoostInvariantTest invariants:\n[PASS] invariant_boostNeverAboveHalfOfBase\n[PASS] invariant_boostWithinBucketAndCaps\n[PASS] invariant_hamsterAccountedAndConserved\n[PASS] invariant_weightMatchesBalances\n BoostInvariantTest invariants (runs: 48, calls: 2880, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| BoostHandler | burnOrRemint     | 274   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | claim            | 290   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | finalize         | 308   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | register         | 296   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | stakeFromBalance | 294   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | transferNft      | 250   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | unstakeAll       | 288   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | voidSqueek       | 269   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | warp             | 298   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| BoostHandler | withdrawVested   | 313   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 234\n  Bound result 2\n  Bound result 24\n  Bound result 21\n  Bound result 18\n  Bound result 217\n  Bound result 6\n  Bound result 4\n  Bound result 1\n  Bound result 21\n  Bound result 17\n  Bound result 19999999999999999951\n  Bound result 2802\n  Bound result 8\n  Bound result 17031\n  Bound result 2\n  Bound result 16\n  Bound result 16\n  Bound result 19999999999990625001\n  Bound result 19999999999999998801\n  Bound result 264\n  Bound result 16\n  Bound result 8\n  Bound result 5413\n  Bound result 2597\n  Bound result 4\n  Bound result 121600\n  Bound result 7\n  Bound result 272\n  Bound result 7\n  Bound result 8\n  Bound result 24\n  Bound result 10642\n  Bound result 252\n  Bound result 24\n  Bound result 19999999999999999601\n  Bound result 1456\n  Bound result 5\n  Bound result 9\n  Bound result 307\n  Bound result 10\n  Bound result 24\n  Bound result 16\n  Bound result 15\n  Bound result 15\n  Bound result 17\n  Bound result 39999\n  Bound result 81123\n  Bound result 295\n  Bound result 18\n  Bound result 20\n  Bound result 290\n  Bound result 11\n  Bound result 24\n  Bound result 3\n  Bound result 40000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 728.69ms (719.26ms CPU time)\n\nRan 7 test suites in 731.08ms (1.58s CPU time): 134 tests passed, 0 failed, 0 skipped (134 total tests)\n","passed":true},{"durationMs":43,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.cancelPendingStake()\",\"HamsterVault.claimMined(uint256[],address)\",\"HamsterVault.claimRewards()\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.pullFromGame()\",\"HamsterVault.registerSqueek(uint256)\",\"HamsterVault.requestUnstake(uint256)\",\"HamsterVault.scheduleBoost(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepBoost()\",\"HamsterVault.sync()\",\"HamsterVault.unregisterSqueek(uint256)\",\"HamsterVault.voidSqueek(uint256)\",\"HamsterVault.withdrawUnstaked()\",\"HamsterVault.withdrawVested(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":184,\"abi/HamsterVault.json\":2181,\"foundry.toml\":14,\"remappings.txt\":1,\"script/Deploy.s.sol\":24,\"src/HamsterVault.sol\":960,\"src/interfaces/IERC20.sol\":9,\"src/interfaces/IERC721.sol\":7,\"src/interfaces/IHamsterHour.sol\":22,\"test/Boost.t.sol\":346,\"test/Deploy.t.sol\":38,\"test/Mining.t.sol\":309,\"test/Staking.t.sol\":472,\"test/VaultEdges.t.sol\":819,\"test/helpers/Mocks.sol\":138,\"test/helpers/VaultTest.sol\":97,\"test/invariant/BoostInvariant.t.sol\":249,\"test/invariant/VaultInvariant.t.sol\":195},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"4162b2ad6e418b9825fc3d152d6a1664710a0b5405bfef5054abc2c1d45c3eab","verifiedTreeHash":"2168048e64a3c6730e7220323333a985e7c14069","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":4192,"exitCode":0,"name":"build","output":"Compiling 30 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.00s\nCompiler run successful!\n","passed":true},{"durationMs":335,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_constructorRejectsZero() (gas: 3903)\n[PASS] test_deployWithRealParamsOnEmptyChain() (gas: 9251723)\n[PASS] test_noOwnerPowersBeyondTwo() (gas: 71176)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 58.40ms (7.44ms CPU time)\n\nRan 28 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_imdConservation(uint256,uint256,uint256,uint256,uint32) (runs: 256, μ: 1385926, ~: 1144121)\nLogs:\n  Bound result 999999000000000000002835\n  Bound result 999999000000000000002459\n  Bound result 36045\n  Bound result 1000000000000000000000000\n\n[PASS] test_activationIsExactWithoutPoke() (gas: 1020476)\n[PASS] test_backlogStreamsOver30Days() (gas: 1173130)\n[PASS] test_bucketsNeverCountAsWeight() (gas: 281790)\n[PASS] test_cancelPendingStake() (gas: 667357)\n[PASS] test_cancelThenRestakeKeepsBucketsConsistent() (gas: 760730)\n[PASS] test_claimRewardsNothingReverts() (gas: 58883)\n[PASS] test_depositRewardsMergesLeftover() (gas: 855340)\n[PASS] test_depositZeroReverts() (gas: 62165)\n[PASS] test_dripFactorAccuracy() (gas: 7885)\n[PASS] test_dripNotPokeable() (gas: 4124858)\n[PASS] test_dripOnePercentPerDay() (gas: 967181)\n[PASS] test_dustDepositsCannotStretchStream() (gas: 2102495)\n[PASS] test_gapBetweenStakersCarriesRewards() (gas: 1299237)\n[PASS] test_grantsAreNotTransferableOrStakeable() (gas: 475071)\n[PASS] test_lazyActivationCreditsFromActivationTime() (gas: 781093)\n[PASS] test_manyDueActivationsNeverBlockDeposits() (gas: 52648456)\n[PASS] test_multipleGrantsCursor() (gas: 1059828)\n[PASS] test_proRataByWeight() (gas: 1252237)\n[PASS] test_restakeDuringWarmupRestarts() (gas: 768140)\n[PASS] test_stakeBelowMinimumReverts() (gas: 139815)\n[PASS] test_unstakeMoreThanActiveReverts() (gas: 350587)\n[PASS] test_unstakeQueueOrder() (gas: 824956)\n[PASS] test_unstakeStopsWeightAndReleasesAfter24h() (gas: 1158511)\n[PASS] test_vestingCountsAsWeightAndVestsLinearly() (gas: 1250578)\n[PASS] test_warmupEndsOnNextWindowBoundary() (gas: 514944)\n[PASS] test_warmupThenWeight() (gas: 840940)\n[PASS] test_zeroStakerPeriodWaitsNoWindfall() (gas: 940247)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 75.64ms (153.98ms CPU time)\n\nRan 20 tests for test/Boost.t.sol:BoostTest\n[PASS] test_activateNotTeamReverts() (gas: 34690)\n[PASS] test_activateRejectsNonErc721() (gas: 190662)\n[PASS] test_activateRequiresScheduleAndTimelock() (gas: 268291)\n[PASS] test_batchClampsAtBoostBucketEnd() (gas: 15045054)\n[PASS] test_boostBucketConservation() (gas: 13139431)\n[PASS] test_boostEarnsOnlyAfterLead() (gas: 13358083)\n[PASS] test_claimObservingTransferVoidsPermanently() (gas: 13278036)\n[PASS] test_globalDailyCap() (gas: 17619361)\n[PASS] test_newOwnerCanRegisterAfterTransfer() (gas: 449855)\n[PASS] test_perNftDailyCap() (gas: 13839630)\n[PASS] test_reRegisterDoesNotDuplicateList() (gas: 526067)\n[PASS] test_registerRequiresActiveAndOwnership() (gas: 486833)\n[PASS] test_sweepBeforeBindThenBind() (gas: 725075)\n[PASS] test_sweepBlockedOnceActive() (gas: 171190)\n[PASS] test_sweepMovesBucketOnce() (gas: 199536)\n[PASS] test_sweepTooEarly() (gas: 39863)\n[PASS] test_transferOutAndBackStaysVoid() (gas: 13174661)\n[PASS] test_transferVoidsRegistration() (gas: 13248750)\n[PASS] test_twoNftsDoubleTheCap() (gas: 13310160)\n[PASS] test_voidSqueekChecks() (gas: 428217)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 124.33ms (253.57ms CPU time)\n\nRan 25 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_conservationOfMiningBucket(uint256[8],uint8) (runs: 256, μ: 3548651, ~: 3286877)\nLogs:\n  Bound result 40000000000000000000000\n  Bound result 25199771348759745223365\n  Bound result 6832950\n  Bound result 2821013684568973272040\n  Bound result 40000000000000000000000\n  Bound result 10737528382502949176831\n  Bound result 3689547584465998562866\n  Bound result 22109433867127032231888\n\n[PASS] testFuzz_curveMonotoneAndBounded(uint256,uint256) (runs: 256, μ: 22842, ~: 22327)\nLogs:\n  Bound result 0\n  Bound result 1\n\n[PASS] test_bindAcceptsDonatedSurplus() (gas: 747314)\n[PASS] test_bindNotTeamReverts() (gas: 35322)\n[PASS] test_bindSetsBuckets() (gas: 56854)\n[PASS] test_bindTwiceReverts() (gas: 36813)\n[PASS] test_bindWrongRemainderReverts() (gas: 611585)\n[PASS] test_bindWrongSupplyReverts() (gas: 558958)\n[PASS] test_claimAlwaysPaysAccountNotCaller() (gas: 461822)\n[PASS] test_claimBatchTooLargeReverts() (gas: 50984)\n[PASS] test_claimBeforeBindReverts() (gas: 182587)\n[PASS] test_claimNothingReverts() (gas: 224744)\n[PASS] test_claimOrderIndependence() (gas: 2209449)\n[PASS] test_claimTwiceReverts() (gas: 472722)\n[PASS] test_claimUnfinalizedReverts() (gas: 189288)\n[PASS] test_claimableViewMatchesClaim() (gas: 515902)\n[PASS] test_curveConvergesToBucket() (gas: 18124)\n[PASS] test_curveEra0Rate() (gas: 17919)\n[PASS] test_curveHalves() (gas: 27072)\n[PASS] test_dustTracked() (gas: 1688525)\n[PASS] test_fullCurveNeverExceedsBucket() (gas: 4037067)\n[PASS] test_halvingCrossingWithinWindow() (gas: 378777)\n[PASS] test_hedgedTotalDoesNotOverDistribute() (gas: 693705)\n[PASS] test_rateView() (gas: 197538)\n[PASS] test_retroactiveClaimFromWindowZero() (gas: 1383361)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 149.92ms (182.79ms CPU time)\n\nRan 4 test suites in 153.23ms (408.29ms CPU time): 76 tests passed, 0 failed, 0 skipped (76 total tests)\n","passed":true},{"durationMs":122,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.cancelPendingStake()\",\"HamsterVault.claimMined(uint256[],address)\",\"HamsterVault.claimRewards()\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.pullFromGame()\",\"HamsterVault.registerSqueek(uint256)\",\"HamsterVault.requestUnstake(uint256)\",\"HamsterVault.scheduleBoost(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepBoost()\",\"HamsterVault.sync()\",\"HamsterVault.unregisterSqueek(uint256)\",\"HamsterVault.voidSqueek(uint256)\",\"HamsterVault.withdrawUnstaked()\",\"HamsterVault.withdrawVested(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":184,\"abi/HamsterVault.json\":2181,\"foundry.toml\":14,\"remappings.txt\":1,\"script/Deploy.s.sol\":24,\"src/HamsterVault.sol\":960,\"src/interfaces/IERC20.sol\":9,\"src/interfaces/IERC721.sol\":7,\"src/interfaces/IHamsterHour.sol\":22,\"test/Boost.t.sol\":346,\"test/Deploy.t.sol\":38,\"test/Mining.t.sol\":309,\"test/Staking.t.sol\":472,\"test/helpers/Mocks.sol\":138,\"test/helpers/VaultTest.sol\":97},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3026,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/HamsterVault.sol:944: Reentrancy in HamsterVault._pull(address,address,uint256) (src/HamsterVault.sol#944-948):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:485: Reentrancy in HamsterVault.pullFromGame() (src/HamsterVault.sol#485-500):\n[high/high] uninitialized-state at src/HamsterVault.sol:658: HamsterVault._squeeks (src/HamsterVault.sol#140) is never initialized. It is used in:\n[medium/medium] divide-before-multiply at src/HamsterVault.sol:238: HamsterVault.emissionAt(uint256) (src/HamsterVault.sol#238-242) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/HamsterVault.sol:387: HamsterVault.activationTime(uint256) (src/HamsterVault.sol#387-391) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/HamsterVault.sol:596: HamsterVault._decay(uint256) (src/HamsterVault.sol#596-604) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/HamsterVault.sol:289: HamsterVault._baseShare(uint256,address) (src/HamsterVault.sol#289-302) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/HamsterVault.sol:334: HamsterVault.withdrawVested(uint256) (src/HamsterVault.sol#334-363) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterVault.sol:596: HamsterVault._decay(uint256) (src/HamsterVault.sol#596-604) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterVault.sol:485: HamsterVault.pullFromGame() (src/HamsterVault.sol#485-500) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:475: Reentrancy in HamsterVault.depositDrip(uint256) (src/HamsterVault.sol#475-481):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:485: Reentrancy in HamsterVault.pullFromGame() (src/HamsterVault.sol#485-500):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:369: Reentrancy in HamsterVault.stake(uint256) (src/HamsterVault.sol#369-384):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:466: Reentrancy in HamsterVault.depositRewards(uint256) (src/HamsterVault.sol#466-472):\n[medium/medium] uninitialized-local at src/HamsterVault.sol:430: HamsterVault.withdrawUnstaked().due (src/HamsterVault.sol#430) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HamsterVault.sol:261: HamsterVault.claimMined(uint256[],address).boost (src/HamsterVault.sol#261) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HamsterVault.sol:260: HamsterVault.claimMined(uint256[],address).base (src/HamsterVault.sol#260) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HamsterVault.sol:339: HamsterVault.withdrawVested(uint256).due (src/HamsterVault.sol#339) is a local variable never initialized\n[low/medium] events-maths at src/HamsterVault.sol:394: HamsterVault.requestUnstake(uint256) (src/HamsterVault.sol#394-403) should emit an event for: \n[low/medium] calls-loop at src/HamsterVault.sol:698: HamsterVault._ownerOf(uint256) (src/HamsterVault.sol#698-702) has external calls inside a loop: (ok,ret) = address(collection).staticcall(abi.encodeCall(IERC721.ownerOf,(tokenId))) (src/HamsterVault.sol#699)\n[low/medium] calls-loop at src/HamsterVault.sol:289: HamsterVault._baseShare(uint256,address) (src/HamsterVault.sol#289-302) has external calls inside a loop: loss = GAME.lossOf(k,account) (src/HamsterVault.sol#292)\n[low/medium] calls-loop at src/HamsterVault.sol:698: HamsterVault._ownerOf(uint256) (src/HamsterVault.sol#698-702) has external calls inside a loop: (ok,ret) = address(collection).staticcall(abi.encodeCall(IERC721.ownerOf,(tokenId))) (src/HamsterVault.sol#699)\n[low/medium] calls-loop at src/HamsterVault.sol:289: HamsterVault._baseShare(uint256,address) (src/HamsterVault.sol#289-302) has external calls inside a loop: total = GAME.totalEligibleLoss(k) (src/HamsterVault.sol#290)\n[low/medium] calls-loop at src/HamsterVault.sol:245: HamsterVault.windowBudget(uint256) (src/HamsterVault.sol#245-247) has external calls inside a loop: emissionAt(GAME.cumulativeLossAfter(k)) - emissionAt(GAME.cumulativeLossBefore(k)) (src/HamsterVault.sol#246)\n[low/medium] reentrancy-benign at src/HamsterVault.sol:485: Reentrancy in HamsterVault.pullFromGame() (src/HamsterVault.sol#485-500):\n[low/medium] reentrancy-benign at src/HamsterVault.sol:369: Reentrancy in HamsterVault.stake(uint256) (src/HamsterVault.sol#369-384):\n[low/medium] timestamp at src/HamsterVault.sol:518: HamsterVault._sync() (src/HamsterVault.sol#518-533) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:911: HamsterVault._flowPreview(HamsterVault.Stream,uint64,uint64) (src/HamsterVault.sol#911-915) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:325: HamsterVault.withdrawableVested(address) (src/HamsterVault.sol#325-330) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:901: HamsterVault._projectedRewardPerWeight() (src/HamsterVault.sol#901-909) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:502: HamsterVault._addToStream(HamsterVault.Stream,uint256,uint64) (src/HamsterVault.sol#502-512) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:637: HamsterVault.sweepBoost() (src/HamsterVault.sol#637-649) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:858: HamsterVault.stakeInfo(address) (src/HamsterVault.sol#858-878) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:737: HamsterVault._allocateBoost(uint256,address,uint256,uint256) (src/HamsterVault.sol#737-771) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:886: HamsterVault.pendingRewards(address) (src/HamsterVault.sol#886-899) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:574: HamsterVault._settle(address) (src/HamsterVault.sol#574-593) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:334: HamsterVault.withdrawVested(uint256) (src/HamsterVault.sol#334-363) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:619: HamsterVault.activateBoost(address) (src/HamsterVault.sol#619-633) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:561: HamsterVault._flow(HamsterVault.Stream,uint64,uint64) (src/HamsterVault.sol#561-570) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:427: HamsterVault.withdrawUnstaked() (src/HamsterVault.sol#427-441) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:319: HamsterVault.vestedOf(HamsterVault.Grant) (src/HamsterVault.sol#319-322) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:369: HamsterVault.stake(uint256) (src/HamsterVault.sol#369-384) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:596: HamsterVault._decay(uint256) (src/HamsterVault.sol#596-604) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:537: HamsterVault._advance(uint64) (src/HamsterVault.sol#537-559) uses timestamp for comparisons","passed":true},{"durationMs":931,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/HamsterVault.sol:225: Reentrancy: State change after external call (7 places)\n[high] storage-array-memory-edit at src/HamsterVault.sol:328: Storage Array Edited with Memory\n[high] unsafe-casting at src/HamsterVault.sol:422: Unsafe Casting of integers\n[low] costly-loop at src/HamsterVault.sol:262: Costly operations inside loop (6 places)\n[low] division-before-multiplication at src/HamsterVault.sol:390: Incorrect Order of Division and Multiplication\n[low] large-numeric-literal at src/HamsterVault.sol:27: Large Numeric Literal\n[low] literal-instead-of-constant at src/HamsterVault.sol:268: Literal Instead of Constant (5 places)\n[low] local-variable-shadowing at src/HamsterVault.sol:799: Local Variable Shadows State Variable (11 places)\n[low] require-revert-in-loop at src/HamsterVault.sol:262: Loop Contains `require`/`revert`\n[low] state-change-without-event at src/HamsterVault.sol:458: State Change Without Event\n[low] uninitialized-local-variable at src/HamsterVault.sol:262: Uninitialized Local Variable (3 places)\n[low] unsafe-erc20-operation at src/HamsterVault.sol:946: Unsafe ERC20 Operation (2 places)","passed":true},{"durationMs":2926,"exitCode":0,"name":"proof 94b192a1ac44","output":"Compiling 25 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.73s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-8b12c86e/Proof_94b192a1ac44.t.sol:BindDonationDoSTest\n[PASS] test_donationBeforeBindDoesNotLockVaultForever() (gas: 196354)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.16ms (136.73µs CPU time)\n\nRan 1 test suite in 6.23ms (1.16ms 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":"4f9f93683c5530c5d4aab46591ab584088f403ebdd34c0afd7360836352bd516","verifiedTreeHash":"4abce6093e32aae84d67d0217f46d72b4f6370d8","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":3426,"exitCode":0,"name":"build","output":"Compiling 30 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.30s\nCompiler run successful!\n","passed":true},{"durationMs":267,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_constructorRejectsZero() (gas: 3903)\n[PASS] test_deployWithRealParamsOnEmptyChain() (gas: 8573200)\n[PASS] test_noOwnerPowersBeyondTwo() (gas: 71132)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 2.74ms (3.45ms CPU time)\n\nRan 15 tests for test/Boost.t.sol:BoostTest\n[PASS] test_activateNotTeamReverts() (gas: 34624)\n[PASS] test_activateRejectsNonErc721() (gas: 190574)\n[PASS] test_activateRequiresScheduleAndTimelock() (gas: 268203)\n[PASS] test_boostBucketConservation() (gas: 13135171)\n[PASS] test_boostEarnsOnlyAfterLead() (gas: 13353952)\n[PASS] test_globalDailyCap() (gas: 17573941)\n[PASS] test_newOwnerCanRegisterAfterTransfer() (gas: 447868)\n[PASS] test_perNftDailyCap() (gas: 13827760)\n[PASS] test_registerRequiresActiveAndOwnership() (gas: 486798)\n[PASS] test_sweepBeforeBindThenBind() (gas: 725026)\n[PASS] test_sweepBlockedOnceActive() (gas: 171212)\n[PASS] test_sweepMovesBucketOnce() (gas: 199402)\n[PASS] test_sweepTooEarly() (gas: 39974)\n[PASS] test_transferVoidsRegistration() (gas: 13258446)\n[PASS] test_twoNftsDoubleTheCap() (gas: 13301277)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 39.67ms (99.13ms CPU time)\n\nRan 24 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_imdConservation(uint256,uint256,uint256,uint256,uint32) (runs: 256, μ: 1064451, ~: 1064393)\nLogs:\n  Bound result 999999000000000000002833\n  Bound result 999999000000000000031963\n  Bound result 3988\n  Bound result 24576\n\n[PASS] test_activationIsExactWithoutPoke() (gas: 1029000)\n[PASS] test_backlogStreamsOver30Days() (gas: 1154725)\n[PASS] test_bucketsNeverCountAsWeight() (gas: 282085)\n[PASS] test_cancelPendingStake() (gas: 617441)\n[PASS] test_claimRewardsNothingReverts() (gas: 54862)\n[PASS] test_depositRewardsMergesLeftover() (gas: 843162)\n[PASS] test_depositZeroReverts() (gas: 62320)\n[PASS] test_dripFactorAccuracy() (gas: 7885)\n[PASS] test_dripNotPokeable() (gas: 3784318)\n[PASS] test_dripOnePercentPerDay() (gas: 931797)\n[PASS] test_gapBetweenStakersCarriesRewards() (gas: 1244322)\n[PASS] test_grantsAreNotTransferableOrStakeable() (gas: 471022)\n[PASS] test_multipleGrantsCursor() (gas: 1043405)\n[PASS] test_proRataByWeight() (gas: 1222442)\n[PASS] test_restakeDuringWarmupRestarts() (gas: 717655)\n[PASS] test_stakeBelowMinimumReverts() (gas: 139771)\n[PASS] test_syncRequiredWithBacklog() (gas: 22326951)\n[PASS] test_unstakeMoreThanActiveReverts() (gas: 308195)\n[PASS] test_unstakeQueueOrder() (gas: 785027)\n[PASS] test_unstakeStopsWeightAndReleasesAfter24h() (gas: 1134471)\n[PASS] test_vestingCountsAsWeightAndVestsLinearly() (gas: 1233706)\n[PASS] test_warmupThenWeight() (gas: 799338)\n[PASS] test_zeroStakerPeriodWaitsNoWindfall() (gas: 915145)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 118.54ms (79.43ms CPU time)\n\nRan 24 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_conservationOfMiningBucket(uint256[8],uint8) (runs: 256, μ: 3486734, ~: 3339522)\nLogs:\n  Bound result 34218004887090175677608\n  Bound result 1302122150100681\n  Bound result 2816666343476623043554\n  Bound result 29206677242238528209641\n  Bound result 21779380742503932799988\n  Bound result 1812642129568\n  Bound result 31756190291244720059425\n  Bound result 29674230579332778217158\n\n[PASS] testFuzz_curveMonotoneAndBounded(uint256,uint256) (runs: 256, μ: 22711, ~: 22194)\nLogs:\n  Bound result 156304759387182631679476943574\n  Bound result 249203100024283274674362115123\n\n[PASS] test_bindNotTeamReverts() (gas: 35300)\n[PASS] test_bindSetsBuckets() (gas: 56764)\n[PASS] test_bindTwiceReverts() (gas: 36791)\n[PASS] test_bindWrongRemainderReverts() (gas: 611443)\n[PASS] test_bindWrongSupplyReverts() (gas: 558904)\n[PASS] test_claimAlwaysPaysAccountNotCaller() (gas: 457796)\n[PASS] test_claimBatchTooLargeReverts() (gas: 50984)\n[PASS] test_claimBeforeBindReverts() (gas: 182500)\n[PASS] test_claimNothingReverts() (gas: 224744)\n[PASS] test_claimOrderIndependence() (gas: 2176457)\n[PASS] test_claimTwiceReverts() (gas: 468696)\n[PASS] test_claimUnfinalizedReverts() (gas: 189310)\n[PASS] test_claimableViewMatchesClaim() (gas: 511898)\n[PASS] test_curveConvergesToBucket() (gas: 18080)\n[PASS] test_curveEra0Rate() (gas: 17875)\n[PASS] test_curveHalves() (gas: 27028)\n[PASS] test_dustTracked() (gas: 1668375)\n[PASS] test_fullCurveNeverExceedsBucket() (gas: 4032880)\n[PASS] test_halvingCrossingWithinWindow() (gas: 378755)\n[PASS] test_hedgedTotalDoesNotOverDistribute() (gas: 685631)\n[PASS] test_rateView() (gas: 197492)\n[PASS] test_retroactiveClaimFromWindowZero() (gas: 1378980)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 118.65ms (137.93ms CPU time)\n\nRan 4 test suites in 120.76ms (279.60ms CPU time): 66 tests passed, 0 failed, 0 skipped (66 total tests)\n","passed":true},{"durationMs":84,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.cancelPendingStake()\",\"HamsterVault.claimMined(uint256[],address)\",\"HamsterVault.claimRewards()\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.pullFromGame()\",\"HamsterVault.registerSqueek(uint256)\",\"HamsterVault.requestUnstake(uint256)\",\"HamsterVault.scheduleBoost(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepBoost()\",\"HamsterVault.sync(uint256)\",\"HamsterVault.unregisterSqueek(uint256)\",\"HamsterVault.withdrawUnstaked()\",\"HamsterVault.withdrawVested(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":168,\"abi/HamsterVault.json\":2122,\"foundry.toml\":14,\"remappings.txt\":1,\"script/Deploy.s.sol\":24,\"src/HamsterVault.sol\":873,\"src/interfaces/IERC20.sol\":9,\"src/interfaces/IERC721.sol\":7,\"src/interfaces/IHamsterHour.sol\":22,\"test/Boost.t.sol\":262,\"test/Deploy.t.sol\":38,\"test/Mining.t.sol\":295,\"test/Staking.t.sol\":409,\"test/helpers/Mocks.sol\":138,\"test/helpers/VaultTest.sol\":97},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2416,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/HamsterVault.sol:857: Reentrancy in HamsterVault._pull(address,address,uint256) (src/HamsterVault.sol#857-861):\n[high/medium] reentrancy-balance at src/HamsterVault.sol:468: Reentrancy in HamsterVault.pullFromGame() (src/HamsterVault.sol#468-483):\n[high/high] uninitialized-state at src/HamsterVault.sol:632: HamsterVault._squeeks (src/HamsterVault.sol#139) is never initialized. It is used in:\n[medium/medium] divide-before-multiply at src/HamsterVault.sol:571: HamsterVault._decay(uint256) (src/HamsterVault.sol#571-579) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/HamsterVault.sol:235: HamsterVault.emissionAt(uint256) (src/HamsterVault.sol#235-239) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/HamsterVault.sol:286: HamsterVault._baseShare(uint256,address) (src/HamsterVault.sol#286-299) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/HamsterVault.sol:571: HamsterVault._decay(uint256) (src/HamsterVault.sol#571-579) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterVault.sol:501: HamsterVault._processActivations(uint256) (src/HamsterVault.sol#501-523) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterVault.sol:468: HamsterVault.pullFromGame() (src/HamsterVault.sol#468-483) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HamsterVault.sol:331: HamsterVault.withdrawVested(uint256) (src/HamsterVault.sol#331-360) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:468: Reentrancy in HamsterVault.pullFromGame() (src/HamsterVault.sol#468-483):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:365: Reentrancy in HamsterVault.stake(uint256) (src/HamsterVault.sol#365-377):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:449: Reentrancy in HamsterVault.depositRewards(uint256) (src/HamsterVault.sol#449-455):\n[medium/medium] reentrancy-no-eth at src/HamsterVault.sol:458: Reentrancy in HamsterVault.depositDrip(uint256) (src/HamsterVault.sol#458-464):\n[medium/medium] uninitialized-local at src/HamsterVault.sol:414: HamsterVault.withdrawUnstaked().due (src/HamsterVault.sol#414) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HamsterVault.sol:258: HamsterVault.claimMined(uint256[],address).boost (src/HamsterVault.sol#258) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HamsterVault.sol:257: HamsterVault.claimMined(uint256[],address).base (src/HamsterVault.sol#257) is a local variable never initialized\n[medium/medium] uninitialized-local at src/HamsterVault.sol:336: HamsterVault.withdrawVested(uint256).due (src/HamsterVault.sol#336) is a local variable never initialized\n[low/medium] events-maths at src/HamsterVault.sol:380: HamsterVault.requestUnstake(uint256) (src/HamsterVault.sol#380-389) should emit an event for: \n[low/medium] calls-loop at src/HamsterVault.sol:669: HamsterVault.squeekEligible(uint256,address,uint256) (src/HamsterVault.sol#669-675) has external calls inside a loop: (ok,ret) = address(collection).staticcall(abi.encodeCall(IERC721.ownerOf,(tokenId))) (src/HamsterVault.sol#672)\n[low/medium] calls-loop at src/HamsterVault.sol:286: HamsterVault._baseShare(uint256,address) (src/HamsterVault.sol#286-299) has external calls inside a loop: total = GAME.totalEligibleLoss(k) (src/HamsterVault.sol#287)\n[low/medium] calls-loop at src/HamsterVault.sol:669: HamsterVault.squeekEligible(uint256,address,uint256) (src/HamsterVault.sol#669-675) has external calls inside a loop: (ok,ret) = address(collection).staticcall(abi.encodeCall(IERC721.ownerOf,(tokenId))) (src/HamsterVault.sol#672)\n[low/medium] calls-loop at src/HamsterVault.sol:286: HamsterVault._baseShare(uint256,address) (src/HamsterVault.sol#286-299) has external calls inside a loop: loss = GAME.lossOf(k,account) (src/HamsterVault.sol#289)\n[low/medium] calls-loop at src/HamsterVault.sol:242: HamsterVault.windowBudget(uint256) (src/HamsterVault.sol#242-244) has external calls inside a loop: emissionAt(GAME.cumulativeLossAfter(k)) - emissionAt(GAME.cumulativeLossBefore(k)) (src/HamsterVault.sol#243)\n[low/medium] reentrancy-benign at src/HamsterVault.sol:468: Reentrancy in HamsterVault.pullFromGame() (src/HamsterVault.sol#468-483):\n[low/medium] reentrancy-benign at src/HamsterVault.sol:365: Reentrancy in HamsterVault.stake(uint256) (src/HamsterVault.sol#365-377):\n[low/medium] timestamp at src/HamsterVault.sol:495: HamsterVault._sync() (src/HamsterVault.sol#495-499) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:551: HamsterVault._flow(HamsterVault.Stream,uint64,uint64) (src/HamsterVault.sol#551-560) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:571: HamsterVault._decay(uint256) (src/HamsterVault.sol#571-579) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:411: HamsterVault.withdrawUnstaked() (src/HamsterVault.sol#411-425) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:612: HamsterVault.sweepBoost() (src/HamsterVault.sol#612-624) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:322: HamsterVault.withdrawableVested(address) (src/HamsterVault.sol#322-327) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:594: HamsterVault.activateBoost(address) (src/HamsterVault.sol#594-608) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:527: HamsterVault._advance(uint64) (src/HamsterVault.sol#527-549) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:814: HamsterVault._projectedRewardPerWeight() (src/HamsterVault.sol#814-822) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:331: HamsterVault.withdrawVested(uint256) (src/HamsterVault.sol#331-360) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:784: HamsterVault.stakeInfo(address) (src/HamsterVault.sol#784-799) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:501: HamsterVault._processActivations(uint256) (src/HamsterVault.sol#501-523) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:824: HamsterVault._flowPreview(HamsterVault.Stream,uint64,uint64) (src/HamsterVault.sol#824-828) uses timestamp for comparisons\n[low/medium] timestamp at src/HamsterVault.sol:316: HamsterVault.vestedOf(HamsterVault.Grant) (src/HamsterVault.sol#316-319) uses timestamp for comparisons","passed":true},{"durationMs":648,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/HamsterVault.sol:222: Reentrancy: State change after external call (7 places)\n[high] storage-array-memory-edit at src/HamsterVault.sol:325: Storage Array Edited with Memory\n[high] unsafe-casting at src/HamsterVault.sol:406: Unsafe Casting of integers\n[low] costly-loop at src/HamsterVault.sol:259: Costly operations inside loop (5 places)\n[low] large-numeric-literal at src/HamsterVault.sol:27: Large Numeric Literal\n[low] literal-instead-of-constant at src/HamsterVault.sol:265: Literal Instead of Constant (5 places)\n[low] local-variable-shadowing at src/HamsterVault.sol:725: Local Variable Shadows State Variable (11 places)\n[low] require-revert-in-loop at src/HamsterVault.sol:259: Loop Contains `require`/`revert`\n[low] uninitialized-local-variable at src/HamsterVault.sol:259: Uninitialized Local Variable (5 places)\n[low] unsafe-erc20-operation at src/HamsterVault.sol:859: Unsafe ERC20 Operation (2 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"6016085ebb3a5dae59eeb67eaea4a7e80226f2387e8c5f00c01de0dfe570343c","verifiedTreeHash":"8e5cb628a0bf363dc4bf588992f6f11554b115e6","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":2001,"exitCode":0,"name":"build","output":"Compiling 30 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.92s\nCompiler run successful!\n","passed":true},{"durationMs":168,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_constructorRejectsZero() (gas: 3903)\n[PASS] test_deployWithRealParamsOnEmptyChain() (gas: 9251723)\n[PASS] test_noOwnerPowersBeyondTwo() (gas: 71176)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 14.52ms (1.16ms CPU time)\n\nRan 20 tests for test/Boost.t.sol:BoostTest\n[PASS] test_activateNotTeamReverts() (gas: 34690)\n[PASS] test_activateRejectsNonErc721() (gas: 190662)\n[PASS] test_activateRequiresScheduleAndTimelock() (gas: 268291)\n[PASS] test_batchClampsAtBoostBucketEnd() (gas: 15045054)\n[PASS] test_boostBucketConservation() (gas: 13139431)\n[PASS] test_boostEarnsOnlyAfterLead() (gas: 13358083)\n[PASS] test_claimObservingTransferVoidsPermanently() (gas: 13278036)\n[PASS] test_globalDailyCap() (gas: 17619361)\n[PASS] test_newOwnerCanRegisterAfterTransfer() (gas: 449855)\n[PASS] test_perNftDailyCap() (gas: 13839630)\n[PASS] test_reRegisterDoesNotDuplicateList() (gas: 526067)\n[PASS] test_registerRequiresActiveAndOwnership() (gas: 486833)\n[PASS] test_sweepBeforeBindThenBind() (gas: 725075)\n[PASS] test_sweepBlockedOnceActive() (gas: 171190)\n[PASS] test_sweepMovesBucketOnce() (gas: 199536)\n[PASS] test_sweepTooEarly() (gas: 39863)\n[PASS] test_transferOutAndBackStaysVoid() (gas: 13174661)\n[PASS] test_transferVoidsRegistration() (gas: 13248750)\n[PASS] test_twoNftsDoubleTheCap() (gas: 13310160)\n[PASS] test_voidSqueekChecks() (gas: 428217)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 19.10ms (111.12ms CPU time)\n\nRan 28 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_imdConservation(uint256,uint256,uint256,uint256,uint32) (runs: 256, μ: 1385926, ~: 1144121)\nLogs:\n  Bound result 175415660775172751505225\n  Bound result 965546494601029387943417\n  Bound result 539834039045916488637357\n  Bound result 696301378410825512609166\n\n[PASS] test_activationIsExactWithoutPoke() (gas: 1020476)\n[PASS] test_backlogStreamsOver30Days() (gas: 1173130)\n[PASS] test_bucketsNeverCountAsWeight() (gas: 281790)\n[PASS] test_cancelPendingStake() (gas: 667357)\n[PASS] test_cancelThenRestakeKeepsBucketsConsistent() (gas: 760730)\n[PASS] test_claimRewardsNothingReverts() (gas: 58883)\n[PASS] test_depositRewardsMergesLeftover() (gas: 855340)\n[PASS] test_depositZeroReverts() (gas: 62165)\n[PASS] test_dripFactorAccuracy() (gas: 7885)\n[PASS] test_dripNotPokeable() (gas: 4124858)\n[PASS] test_dripOnePercentPerDay() (gas: 967181)\n[PASS] test_dustDepositsCannotStretchStream() (gas: 2102495)\n[PASS] test_gapBetweenStakersCarriesRewards() (gas: 1299237)\n[PASS] test_grantsAreNotTransferableOrStakeable() (gas: 475071)\n[PASS] test_lazyActivationCreditsFromActivationTime() (gas: 781093)\n[PASS] test_manyDueActivationsNeverBlockDeposits() (gas: 52648456)\n[PASS] test_multipleGrantsCursor() (gas: 1059828)\n[PASS] test_proRataByWeight() (gas: 1252237)\n[PASS] test_restakeDuringWarmupRestarts() (gas: 768140)\n[PASS] test_stakeBelowMinimumReverts() (gas: 139815)\n[PASS] test_unstakeMoreThanActiveReverts() (gas: 350587)\n[PASS] test_unstakeQueueOrder() (gas: 824956)\n[PASS] test_unstakeStopsWeightAndReleasesAfter24h() (gas: 1158511)\n[PASS] test_vestingCountsAsWeightAndVestsLinearly() (gas: 1250578)\n[PASS] test_warmupEndsOnNextWindowBoundary() (gas: 514944)\n[PASS] test_warmupThenWeight() (gas: 840940)\n[PASS] test_zeroStakerPeriodWaitsNoWindfall() (gas: 940247)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 71.17ms (90.54ms CPU time)\n\nRan 25 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_conservationOfMiningBucket(uint256[8],uint8) (runs: 256, μ: 3548651, ~: 3286877)\nLogs:\n  Bound result 34218004887090175677608\n  Bound result 1302122150100681\n  Bound result 2816666343476623043554\n  Bound result 29206677242238528209641\n  Bound result 21779380742503932799988\n  Bound result 1812642129568\n  Bound result 31756190291244720059425\n  Bound result 29674230579332778217158\n\n[PASS] testFuzz_curveMonotoneAndBounded(uint256,uint256) (runs: 256, μ: 22842, ~: 22327)\nLogs:\n  Bound result 156304759387182631679476943574\n  Bound result 249203100024283274674362115123\n\n[PASS] test_bindAcceptsDonatedSurplus() (gas: 747314)\n[PASS] test_bindNotTeamReverts() (gas: 35322)\n[PASS] test_bindSetsBuckets() (gas: 56854)\n[PASS] test_bindTwiceReverts() (gas: 36813)\n[PASS] test_bindWrongRemainderReverts() (gas: 611585)\n[PASS] test_bindWrongSupplyReverts() (gas: 558958)\n[PASS] test_claimAlwaysPaysAccountNotCaller() (gas: 461822)\n[PASS] test_claimBatchTooLargeReverts() (gas: 50984)\n[PASS] test_claimBeforeBindReverts() (gas: 182587)\n[PASS] test_claimNothingReverts() (gas: 224744)\n[PASS] test_claimOrderIndependence() (gas: 2209449)\n[PASS] test_claimTwiceReverts() (gas: 472722)\n[PASS] test_claimUnfinalizedReverts() (gas: 189288)\n[PASS] test_claimableViewMatchesClaim() (gas: 515902)\n[PASS] test_curveConvergesToBucket() (gas: 18124)\n[PASS] test_curveEra0Rate() (gas: 17919)\n[PASS] test_curveHalves() (gas: 27072)\n[PASS] test_dustTracked() (gas: 1688525)\n[PASS] test_fullCurveNeverExceedsBucket() (gas: 4037067)\n[PASS] test_halvingCrossingWithinWindow() (gas: 378777)\n[PASS] test_hedgedTotalDoesNotOverDistribute() (gas: 693705)\n[PASS] test_rateView() (gas: 197538)\n[PASS] test_retroactiveClaimFromWindowZero() (gas: 1383361)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 79.56ms (89.62ms CPU time)\n\nRan 4 test suites in 80.42ms (184.35ms CPU time): 76 tests passed, 0 failed, 0 skipped (76 total tests)\n","passed":true},{"durationMs":38,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.cancelPendingStake()\",\"HamsterVault.claimMined(uint256[],address)\",\"HamsterVault.claimRewards()\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.pullFromGame()\",\"HamsterVault.registerSqueek(uint256)\",\"HamsterVault.requestUnstake(uint256)\",\"HamsterVault.scheduleBoost(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepBoost()\",\"HamsterVault.sync()\",\"HamsterVault.unregisterSqueek(uint256)\",\"HamsterVault.voidSqueek(uint256)\",\"HamsterVault.withdrawUnstaked()\",\"HamsterVault.withdrawVested(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":184,\"abi/HamsterVault.json\":2181,\"foundry.toml\":14,\"launch.json\":13,\"remappings.txt\":1,\"script/Deploy.s.sol\":24,\"src/HamsterVault.sol\":960,\"src/interfaces/IERC20.sol\":9,\"src/interfaces/IERC721.sol\":7,\"src/interfaces/IHamsterHour.sol\":22,\"test/Boost.t.sol\":346,\"test/Deploy.t.sol\":38,\"test/Mining.t.sol\":309,\"test/Staking.t.sol\":472,\"test/helpers/Mocks.sol\":138,\"test/helpers/VaultTest.sol\":97},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"63b51e740b8daa8f01cb7d20e0f18857fb496de25df77eac1b553bc0c258d244","verifiedTreeHash":"56b10fcd63b0acc0047edbb6bcfb31d980e2d4f8","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":4701,"exitCode":0,"name":"build","output":"Compiling 31 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.54s\nCompiler run successful!\n","passed":true},{"durationMs":1028,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_constructorRejectsZero() (gas: 3903)\n[PASS] test_deployWithRealParamsOnEmptyChain() (gas: 8573200)\n[PASS] test_noOwnerPowersBeyondTwo() (gas: 71132)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 32.65ms (5.50ms CPU time)\n\nRan 15 tests for test/Boost.t.sol:BoostTest\n[PASS] test_activateNotTeamReverts() (gas: 34624)\n[PASS] test_activateRejectsNonErc721() (gas: 190574)\n[PASS] test_activateRequiresScheduleAndTimelock() (gas: 268203)\n[PASS] test_boostBucketConservation() (gas: 13135171)\n[PASS] test_boostEarnsOnlyAfterLead() (gas: 13353952)\n[PASS] test_globalDailyCap() (gas: 17573941)\n[PASS] test_newOwnerCanRegisterAfterTransfer() (gas: 447868)\n[PASS] test_perNftDailyCap() (gas: 13827760)\n[PASS] test_registerRequiresActiveAndOwnership() (gas: 486798)\n[PASS] test_sweepBeforeBindThenBind() (gas: 725026)\n[PASS] test_sweepBlockedOnceActive() (gas: 171212)\n[PASS] test_sweepMovesBucketOnce() (gas: 199402)\n[PASS] test_sweepTooEarly() (gas: 39974)\n[PASS] test_transferVoidsRegistration() (gas: 13258446)\n[PASS] test_twoNftsDoubleTheCap() (gas: 13301277)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 32.86ms (112.97ms CPU time)\n\nRan 24 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_imdConservation(uint256,uint256,uint256,uint256,uint32) (runs: 256, μ: 1064463, ~: 1064387)\nLogs:\n  Bound result 175415660775172751505225\n  Bound result 965546494601029387943417\n  Bound result 539834039045916488637357\n  Bound result 696301378410825512609166\n\n[PASS] test_activationIsExactWithoutPoke() (gas: 1029000)\n[PASS] test_backlogStreamsOver30Days() (gas: 1154725)\n[PASS] test_bucketsNeverCountAsWeight() (gas: 282085)\n[PASS] test_cancelPendingStake() (gas: 617441)\n[PASS] test_claimRewardsNothingReverts() (gas: 54862)\n[PASS] test_depositRewardsMergesLeftover() (gas: 843162)\n[PASS] test_depositZeroReverts() (gas: 62320)\n[PASS] test_dripFactorAccuracy() (gas: 7885)\n[PASS] test_dripNotPokeable() (gas: 3784318)\n[PASS] test_dripOnePercentPerDay() (gas: 931797)\n[PASS] test_gapBetweenStakersCarriesRewards() (gas: 1244322)\n[PASS] test_grantsAreNotTransferableOrStakeable() (gas: 471022)\n[PASS] test_multipleGrantsCursor() (gas: 1043405)\n[PASS] test_proRataByWeight() (gas: 1222442)\n[PASS] test_restakeDuringWarmupRestarts() (gas: 717655)\n[PASS] test_stakeBelowMinimumReverts() (gas: 139771)\n[PASS] test_syncRequiredWithBacklog() (gas: 22326951)\n[PASS] test_unstakeMoreThanActiveReverts() (gas: 308195)\n[PASS] test_unstakeQueueOrder() (gas: 785027)\n[PASS] test_unstakeStopsWeightAndReleasesAfter24h() (gas: 1134471)\n[PASS] test_vestingCountsAsWeightAndVestsLinearly() (gas: 1233706)\n[PASS] test_warmupThenWeight() (gas: 799338)\n[PASS] test_zeroStakerPeriodWaitsNoWindfall() (gas: 915145)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 50.20ms (98.95ms CPU time)\n\nRan 24 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_conservationOfMiningBucket(uint256[8],uint8) (runs: 256, μ: 3457451, ~: 3208150)\nLogs:\n  Bound result 34218004887090175677608\n  Bound result 1302122150100681\n  Bound result 2816666343476623043554\n  Bound result 29206677242238528209641\n  Bound result 21779380742503932799988\n  Bound result 1812642129568\n  Bound result 31756190291244720059425\n  Bound result 29674230579332778217158\n\n[PASS] testFuzz_curveMonotoneAndBounded(uint256,uint256) (runs: 256, μ: 22716, ~: 22759)\nLogs:\n  Bound result 0\n  Bound result 1\n\n[PASS] test_bindNotTeamReverts() (gas: 35300)\n[PASS] test_bindSetsBuckets() (gas: 56764)\n[PASS] test_bindTwiceReverts() (gas: 36791)\n[PASS] test_bindWrongRemainderReverts() (gas: 611443)\n[PASS] test_bindWrongSupplyReverts() (gas: 558904)\n[PASS] test_claimAlwaysPaysAccountNotCaller() (gas: 457796)\n[PASS] test_claimBatchTooLargeReverts() (gas: 50984)\n[PASS] test_claimBeforeBindReverts() (gas: 182500)\n[PASS] test_claimNothingReverts() (gas: 224744)\n[PASS] test_claimOrderIndependence() (gas: 2176457)\n[PASS] test_claimTwiceReverts() (gas: 468696)\n[PASS] test_claimUnfinalizedReverts() (gas: 189310)\n[PASS] test_claimableViewMatchesClaim() (gas: 511898)\n[PASS] test_curveConvergesToBucket() (gas: 18080)\n[PASS] test_curveEra0Rate() (gas: 17875)\n[PASS] test_curveHalves() (gas: 27028)\n[PASS] test_dustTracked() (gas: 1668375)\n[PASS] test_fullCurveNeverExceedsBucket() (gas: 4032880)\n[PASS] test_halvingCrossingWithinWindow() (gas: 378755)\n[PASS] test_hedgedTotalDoesNotOverDistribute() (gas: 685631)\n[PASS] test_rateView() (gas: 197492)\n[PASS] test_retroactiveClaimFromWindowZero() (gas: 1378980)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 161.56ms (192.03ms CPU time)\n\nRan 1 test for test/invariant/VaultInvariant.t.sol:VaultInvariantTest\n[PASS]\nVaultInvariantTest invariants:\n[PASS] invariant_bucketsNeverExceeded\n[PASS] invariant_hamsterSolvent\n[PASS] invariant_hamsterSupplyFixed\n[PASS] invariant_imdSolventAndConserved\n[PASS] invariant_pendingRewardsCovered\n VaultInvariantTest invariants (runs: 64, calls: 2560, reverts: 4)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | cancelPending    | 213   | 4       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claimMined       | 228   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | claimRewards     | 200   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositDrip      | 209   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | depositRewards   | 187   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | finalize         | 218   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | gameFees         | 200   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | requestUnstake   | 204   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | stake            | 247   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | warp             | 219   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawUnstaked | 225   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | withdrawVested   | 210   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 187607\n  Bound result 500006999999999999999993\n  Bound result 11843\n  Bound result 180000000000000000000000\n  Bound result 2830\n  Bound result 233600\n  Bound result 382\n  Bound result 700000000000000000000\n  Bound result 79219\n  Bound result 1000000000000000000\n  Bound result 1621\n  Bound result 302400\n  Bound result 2742\n  Bound result 11\n  Bound result 5375048135497692576\n  Bound result 7000000000000000000\n  Bound result 43200\n  Bound result 11\n  Bound result 4291\n  Bound result 60000\n  Bound result 10801\n  Bound result 14540\n  Bound result 2986166992\n  Bound result 50000000000000000000\n  Bound result 243\n  Bound result 57849\n  Bound result 8392\n  Bound result 3\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 848.50ms (839.65ms CPU time)\n\nRan 5 test suites in 856.07ms (1.13s CPU time): 67 tests passed, 0 failed, 0 skipped (67 total tests)\n","passed":true},{"durationMs":97,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.cancelPendingStake()\",\"HamsterVault.claimMined(uint256[],address)\",\"HamsterVault.claimRewards()\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.pullFromGame()\",\"HamsterVault.registerSqueek(uint256)\",\"HamsterVault.requestUnstake(uint256)\",\"HamsterVault.scheduleBoost(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepBoost()\",\"HamsterVault.sync(uint256)\",\"HamsterVault.unregisterSqueek(uint256)\",\"HamsterVault.withdrawUnstaked()\",\"HamsterVault.withdrawVested(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":168,\"abi/HamsterVault.json\":2122,\"foundry.toml\":14,\"remappings.txt\":1,\"script/Deploy.s.sol\":24,\"src/HamsterVault.sol\":873,\"src/interfaces/IERC20.sol\":9,\"src/interfaces/IERC721.sol\":7,\"src/interfaces/IHamsterHour.sol\":22,\"test/Boost.t.sol\":262,\"test/Deploy.t.sol\":38,\"test/Mining.t.sol\":295,\"test/Staking.t.sol\":409,\"test/helpers/Mocks.sol\":138,\"test/helpers/VaultTest.sol\":97,\"test/invariant/VaultInvariant.t.sol\":195},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"bf07c42cbae571f4e1fb62fef4968fabbf124e47f4b7cf62257a74c3c9b756b8","verifiedTreeHash":"8d8acc415446b6fb8041cda652c583f9cade159e","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":1815,"exitCode":0,"name":"build","output":"Compiling 30 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.75s\nCompiler run successful!\n","passed":true},{"durationMs":156,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_constructorRejectsZero() (gas: 3903)\n[PASS] test_deployWithRealParamsOnEmptyChain() (gas: 8573200)\n[PASS] test_noOwnerPowersBeyondTwo() (gas: 71132)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 657.84µs (775.08µs CPU time)\n\nRan 15 tests for test/Boost.t.sol:BoostTest\n[PASS] test_activateNotTeamReverts() (gas: 34624)\n[PASS] test_activateRejectsNonErc721() (gas: 190574)\n[PASS] test_activateRequiresScheduleAndTimelock() (gas: 268203)\n[PASS] test_boostBucketConservation() (gas: 13135171)\n[PASS] test_boostEarnsOnlyAfterLead() (gas: 13353952)\n[PASS] test_globalDailyCap() (gas: 17573941)\n[PASS] test_newOwnerCanRegisterAfterTransfer() (gas: 447868)\n[PASS] test_perNftDailyCap() (gas: 13827760)\n[PASS] test_registerRequiresActiveAndOwnership() (gas: 486798)\n[PASS] test_sweepBeforeBindThenBind() (gas: 725026)\n[PASS] test_sweepBlockedOnceActive() (gas: 171212)\n[PASS] test_sweepMovesBucketOnce() (gas: 199402)\n[PASS] test_sweepTooEarly() (gas: 39974)\n[PASS] test_transferVoidsRegistration() (gas: 13258446)\n[PASS] test_twoNftsDoubleTheCap() (gas: 13301277)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 33.04ms (61.73ms CPU time)\n\nRan 24 tests for test/Mining.t.sol:MiningTest\n[PASS] testFuzz_conservationOfMiningBucket(uint256[8],uint8) (runs: 256, μ: 3486734, ~: 3339522)\nLogs:\n  Bound result 999999883676675127\n  Bound result 1000\n  Bound result 5114\n  Bound result 40000000000000000000000\n  Bound result 3372\n  Bound result 5\n  Bound result 3856056066\n  Bound result 150000000000000000000\n\n[PASS] testFuzz_curveMonotoneAndBounded(uint256,uint256) (runs: 256, μ: 22711, ~: 22194)\nLogs:\n  Bound result 4627\n  Bound result 11428571428571428571428\n\n[PASS] test_bindNotTeamReverts() (gas: 35300)\n[PASS] test_bindSetsBuckets() (gas: 56764)\n[PASS] test_bindTwiceReverts() (gas: 36791)\n[PASS] test_bindWrongRemainderReverts() (gas: 611443)\n[PASS] test_bindWrongSupplyReverts() (gas: 558904)\n[PASS] test_claimAlwaysPaysAccountNotCaller() (gas: 457796)\n[PASS] test_claimBatchTooLargeReverts() (gas: 50984)\n[PASS] test_claimBeforeBindReverts() (gas: 182500)\n[PASS] test_claimNothingReverts() (gas: 224744)\n[PASS] test_claimOrderIndependence() (gas: 2176457)\n[PASS] test_claimTwiceReverts() (gas: 468696)\n[PASS] test_claimUnfinalizedReverts() (gas: 189310)\n[PASS] test_claimableViewMatchesClaim() (gas: 511898)\n[PASS] test_curveConvergesToBucket() (gas: 18080)\n[PASS] test_curveEra0Rate() (gas: 17875)\n[PASS] test_curveHalves() (gas: 27028)\n[PASS] test_dustTracked() (gas: 1668375)\n[PASS] test_fullCurveNeverExceedsBucket() (gas: 4032880)\n[PASS] test_halvingCrossingWithinWindow() (gas: 378755)\n[PASS] test_hedgedTotalDoesNotOverDistribute() (gas: 685631)\n[PASS] test_rateView() (gas: 197492)\n[PASS] test_retroactiveClaimFromWindowZero() (gas: 1378980)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 73.96ms (84.61ms CPU time)\n\nRan 24 tests for test/Staking.t.sol:StakingTest\n[PASS] testFuzz_imdConservation(uint256,uint256,uint256,uint256,uint32) (runs: 256, μ: 1064451, ~: 1064393)\nLogs:\n  Bound result 999999000000000000002833\n  Bound result 999999000000000000031963\n  Bound result 3988\n  Bound result 24576\n\n[PASS] test_activationIsExactWithoutPoke() (gas: 1029000)\n[PASS] test_backlogStreamsOver30Days() (gas: 1154725)\n[PASS] test_bucketsNeverCountAsWeight() (gas: 282085)\n[PASS] test_cancelPendingStake() (gas: 617441)\n[PASS] test_claimRewardsNothingReverts() (gas: 54862)\n[PASS] test_depositRewardsMergesLeftover() (gas: 843162)\n[PASS] test_depositZeroReverts() (gas: 62320)\n[PASS] test_dripFactorAccuracy() (gas: 7885)\n[PASS] test_dripNotPokeable() (gas: 3784318)\n[PASS] test_dripOnePercentPerDay() (gas: 931797)\n[PASS] test_gapBetweenStakersCarriesRewards() (gas: 1244322)\n[PASS] test_grantsAreNotTransferableOrStakeable() (gas: 471022)\n[PASS] test_multipleGrantsCursor() (gas: 1043405)\n[PASS] test_proRataByWeight() (gas: 1222442)\n[PASS] test_restakeDuringWarmupRestarts() (gas: 717655)\n[PASS] test_stakeBelowMinimumReverts() (gas: 139771)\n[PASS] test_syncRequiredWithBacklog() (gas: 22326951)\n[PASS] test_unstakeMoreThanActiveReverts() (gas: 308195)\n[PASS] test_unstakeQueueOrder() (gas: 785027)\n[PASS] test_unstakeStopsWeightAndReleasesAfter24h() (gas: 1134471)\n[PASS] test_vestingCountsAsWeightAndVestsLinearly() (gas: 1233706)\n[PASS] test_warmupThenWeight() (gas: 799338)\n[PASS] test_zeroStakerPeriodWaitsNoWindfall() (gas: 915145)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 74.00ms (53.36ms CPU time)\n\nRan 4 test suites in 74.93ms (181.66ms CPU time): 66 tests passed, 0 failed, 0 skipped (66 total tests)\n","passed":true},{"durationMs":41,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HamsterVault.activateBoost(address)\",\"HamsterVault.bindToken(address)\",\"HamsterVault.cancelPendingStake()\",\"HamsterVault.claimMined(uint256[],address)\",\"HamsterVault.claimRewards()\",\"HamsterVault.depositDrip(uint256)\",\"HamsterVault.depositRewards(uint256)\",\"HamsterVault.pullFromGame()\",\"HamsterVault.registerSqueek(uint256)\",\"HamsterVault.requestUnstake(uint256)\",\"HamsterVault.scheduleBoost(address)\",\"HamsterVault.stake(uint256)\",\"HamsterVault.sweepBoost()\",\"HamsterVault.sync(uint256)\",\"HamsterVault.unregisterSqueek(uint256)\",\"HamsterVault.withdrawUnstaked()\",\"HamsterVault.withdrawVested(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":168,\"abi/HamsterVault.json\":2122,\"foundry.toml\":14,\"launch.json\":13,\"remappings.txt\":1,\"script/Deploy.s.sol\":24,\"src/HamsterVault.sol\":873,\"src/interfaces/IERC20.sol\":9,\"src/interfaces/IERC721.sol\":7,\"src/interfaces/IHamsterHour.sol\":22,\"test/Boost.t.sol\":262,\"test/Deploy.t.sol\":38,\"test/Mining.t.sol\":295,\"test/Staking.t.sol\":409,\"test/helpers/Mocks.sol\":138,\"test/helpers/VaultTest.sol\":97},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e79e4bc12a2d8050c7f19b9f5321841cb4050e3fc3320eef5eec3e9a6aa976fa","verifiedTreeHash":"633d3282447d38b60df0fd1632af916c1cd246c4","verifierVersion":"0.1.0+6a374fcd"}]}