{"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":"ebe84da9-e370-493d-95c0-c593847dddb7","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"c6fec66c5ed7564ac8ce3ff8c594e499e068ef3420ae070321df56af639117df","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":"bd8771e9530d61fc25f4cb1aefe317128c1db005aeb01cbcf27532a12034514e","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":"5a2fcb9d917f6d96bcc1796fdd10ddc1e1397cc4d73918c922e309754dac32fd","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"1d349d80e353ca1606d238c358680db0023d15e693d18d5927afd59c3f3961f4","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":"4a9ac26886462a69b6775fdc0cffe398c165a4faa195380fb430df8d95e9156a","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":"e4e6f11ee9228830037e62fa195c619760bc8fa284743419a69ef80724444d4f","dependsOn":[],"execution":{"mustProduce":["src/LaunchToken.sol"],"network":false,"profile":"foundry","requires":[],"skillHash":"47381b166d1f406a34f0ebdc740ee21bc0383e56e7ad9084b26abf1bf7570904","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"47381b166d1f406a34f0ebdc740ee21bc0383e56e7ad9084b26abf1bf7570904","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"b4a910c69d3d48e7206ae6312c06f83195af87a7d058d43306b8989181f8695e","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"cdb8bd3084d37604df5a29cae9f5799813da61babcc4e733a1b8421fc5d22073","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"A staking vault for the launch token ($token): stakers lock for 7 days, anyone can fund rewards in the token, rewards are paid pro rata to stake per second, and no one can touch stakers' principal. Include unit and invariant tests.","parentJobId":null,"planHash":"32d101f4bc0d15ab474712c2bafc8ff7e76270ec3d80a49102bddd8d5874c915","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"ebe84da9-e370-493d-95c0-c593847dddb7","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-537-staking-vault-launch-token"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51725","feedbackHash":"f93e02d98a6144ec09580f20e6eee6020d5d1a83bae18feb638273740bf809ec","nodeKey":"audit_economics","submissionHash":"c6fec66c5ed7564ac8ce3ff8c594e499e068ef3420ae070321df56af639117df","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51028","feedbackHash":"3c5a5e9abba22c08ba26c938b051d4d88a2fbe84e5588bfd655ee88841d4aefc","nodeKey":"audit_flow","submissionHash":"bd8771e9530d61fc25f4cb1aefe317128c1db005aeb01cbcf27532a12034514e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50955","feedbackHash":"e260e764752d1e90adb11b9d76372a23ed238dd3bd3909ec093064e393509211","nodeKey":"audit_judge","submissionHash":"5a2fcb9d917f6d96bcc1796fdd10ddc1e1397cc4d73918c922e309754dac32fd","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51018","feedbackHash":"c2f35dcd5d239ff2a4f6dff8769b8140fae6ca64b204bf57a17b8b894cd4f88e","nodeKey":"audit_math","submissionHash":"1d349d80e353ca1606d238c358680db0023d15e693d18d5927afd59c3f3961f4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50953","feedbackHash":"acd8f949f4e49b067ea533bfe527e451d17e0846cd62c3d32b0fbf0b6998502b","nodeKey":"audit_permissions","submissionHash":"4a9ac26886462a69b6775fdc0cffe398c165a4faa195380fb430df8d95e9156a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50974","feedbackHash":"af7ecf2fe0e28e341118afdbbb3cb32e89175747a8ece0cb8690cfab102bfe06","nodeKey":"build_contract_project","submissionHash":"e4e6f11ee9228830037e62fa195c619760bc8fa284743419a69ef80724444d4f","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"b60cf30764469ddb6c6fd9d689026750a7b70870d344f491445874044c4c5a6c","nodeKey":"manifest","submissionHash":"b4a910c69d3d48e7206ae6312c06f83195af87a7d058d43306b8989181f8695e","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50974","feedbackHash":"7cdd6beff571798c8053f919fce615828cd0da371b946d938c0145fd71f40cd0","nodeKey":"write_foundry_tests","submissionHash":"cdb8bd3084d37604df5a29cae9f5799813da61babcc4e733a1b8421fc5d22073","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"c5bb97c76675ed1a88d2912ad5b760fa317b4abe8cc631b3f61906c5c60b4a35","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30a6c1a419ef4f9c","findings":[{"citation":"resolved","description":"Seam: boundary x precision. fundRewards rebuilds the period total from two floor-divided pieces (the unstreamed remainder `currentRate * remaining / PRECISION`, and for an idle vault the `undistributed` accumulator, itself a sum of floor divisions) and then floor-divides again to get `newRate`. Each floor drops sub-wei, so for any funding whose rate does not divide exactly (the normal case; the unit tests only use 604_800 ether, which gives the exact rate 1e36) the recomputed total is 1 wei short of the original and `newRate` lands one `PRECISION/rewardsDuration` unit (about 1.65e12) below `currentRate`. The README (lines 53-54 and 122-123) tells funders that a mid-period top-up must be \"at least what the current period has already streamed\"; a funder who follows that literally and sends exactly rewardRate*elapsed/1e18 is reverted with RewardRateTooLow, as is a zero-amount restart attempted mid-period while nobody is staked (where the real-arithmetic total is unchanged, so the floor is not actually being violated). The rounding only ever lowers newRate, so it never lets a dust deposit slow the stream (the dangerous direction is safe); the effect is a spurious revert at the exact boundary the documentation names, plus a 1-wei-per-funding rounding loss. Impact is bounded to a failed transaction and the gas for it, so low. The test suite does not exercise the rate floor with any reward amount whose rate has a remainder (`REWARD = 604_800 ether` gives exactly 1e36 and the invariant handler skips every fund call the floor would reject), which is why this edge is invisible to it. Minimal fix that preserves the design: expose the required top-up with ceiling rounding, e.g. `function minimumTopUp() external view returns (uint256) { if (block.timestamp >= periodFinish) return 0; uint256 needed = (rewardRate * rewardsDuration + PRECISION - 1) / PRECISION; uint256 rem = (rewardRate * (periodFinish - block.timestamp)) / PRECISION; return needed > rem + undistributed ? needed - rem - undistributed : 0; }` and document that `fundRewards(minimumTopUp())` is the smallest accepted mid-period top-up; or, equivalently, carry the rollover in rate units (`total * PRECISION + currentRate * remaining`) and only divide once, then compare `newRate + 1 < currentRate` to absorb the single-unit floor error.","line":167,"path":"src/StakingVault.sol","reproduction":"State: StakingVault(token, 7 days) at t0 = 1_700_000_000; alice stakes 100 ether; funder calls fundRewards(1000 ether). Stored rewardRate = 1e39 / 604800 = 1653439153439153439153439153439153 (not exact). Warp to t0 + 3 days. Streamed per the README's rule = rewardRate * 259200 / 1e18 = 428571428571428571428; remainingRewards() = rewardRate * 345600 / 1e18 = 571428571428571428571. Call fundRewards(428571428571428571428). Inside: total = 428571428571428571428 + 571428571428571428571 = 999999999999999999999 (one wei short of 1000e18); newRate = 999999999999999999999 * 1e18 / 604800 = 1653439153439153439151785714285714 < currentRate. Expected: the call succeeds and the period restarts at the same rate (the funder put back everything that streamed). Actual: revert RewardRateTooLow(1653439153439153439153439153439153, 1653439153439153439151785714285714). Funding one wei more (428571428571428571429, which equals 1000 ether - remainingRewards()) succeeds and restores rewardRate == 1653439153439153439153439153439153. Second input, same seam: no stakers, fundRewards(1000 ether) at t0, warp t0 + 3 days, fundRewards(0): undistributed = 428571428571428571428, remaining = 571428571428571428571, newRate is the same 1653439153439153439151785714285714 and the call reverts although nothing has been streamed to anyone and the real total is unchanged. Verified with forge test on test/scratch/MathProbe.t.sol (test_topUpExactStreamedReverts, test_zeroFundMidPeriodNobodyStaked).","severity":"low","snippet":"            total += (currentRate * remaining) / PRECISION;\n        }\n        if (total == 0) revert ZeroAmount();\n\n        uint256 newRate = (total * PRECISION) / rewardsDuration;\n        if (active && newRate < currentRate) revert RewardRateTooLow(currentRate, newRate);","title":"Rate floor compares a floor-rounded rollover, so a top-up of exactly the streamed amount reverts by 1 wei of rounding"},{"citation":"resolved","description":"Seam: boundary x precision x invariant. `newRate = (total * PRECISION) / rewardsDuration` (line 171) is only non-zero when total * 1e18 >= rewardsDuration. The constructor rejects 0 but has no upper bound, so a deployment with rewardsDuration_ > 1e18 * total lets fundRewards pull tokens, pass the `total == 0` check, and then store rewardRate = 0 and periodFinish = now + rewardsDuration. Nothing streams, `remainingRewards()` reports 0, the vault's documented invariant `balance >= totalStaked + owed + remaining + undistributed` still holds only because the funded amount has silently dropped out of every liability term, and because the vault has no sweep by design the tokens are stuck forever. A later funding sees `active == true` and `remaining == 0`, so it too is divided by the huge duration and rounds to 0 unless it alone exceeds rewardsDuration / 1e18 tokens. At the far end, rewardsDuration_ close to 2^256 makes `block.timestamp + rewardsDuration` (line 177) overflow, so fundRewards always reverts and the vault can only ever take stake and return it. The launch manifest value is 604800, so this is a deployment-input hazard rather than a live defect; it is reported because the constructor is the only configuration point, the README calls rewardsDuration \"the one deployment choice\" without a bound, and the failure is permanent loss of whatever is funded. Minimal fix preserving the design: bound the parameter in the constructor, e.g. `if (rewardsDuration_ == 0 || rewardsDuration_ > 365 days) revert ZeroDuration();` (or a dedicated `InvalidDuration` error), and/or `if (newRate == 0) revert ZeroAmount();` in fundRewards so a funding that cannot stream is refused instead of absorbed.","line":108,"path":"src/StakingVault.sol","reproduction":"Input 1: deploy StakingVault(token, 1e40). alice stakes 100 ether; funder calls fundRewards(1 ether). total = 1e18; newRate = 1e18 * 1e18 / 1e40 = 0; no revert. State after: rewardRate == 0, remainingRewards() == 0, periodFinish == now + 1e40, token.balanceOf(vault) == 101 ether. Warp 365 days: earned(alice) == 0. Call fundRewards(1 ether) again: active, remaining = 0, newRate = 0 again; balance 102 ether, still nothing streams and there is no function that can move the 2 ether out. Expected: either the constructor rejects the duration or fundRewards refuses a funding whose rate is zero. Actual: the funding is accepted and permanently stranded. Input 2: deploy StakingVault(token, type(uint256).max); fundRewards(1 ether) reverts with an arithmetic overflow at `block.timestamp + rewardsDuration`, and every future call does the same, so the vault can never pay rewards. Verified with forge test on test/scratch/MathProbe.t.sol (test_hugeDurationStrandsRewards, test_maxDurationBricksFunding).","severity":"low","snippet":"        if (token_ == address(0)) revert ZeroAddress();\n        if (rewardsDuration_ == 0) revert ZeroDuration();\n        token = IERC20(token_);\n        rewardsDuration = rewardsDuration_;","title":"Constructor accepts any non-zero rewardsDuration; values above ~1e18 round every reward rate to zero and strand funded tokens, and values near 2^256 brick fundRewards"}],"hash":"1d349d80e353ca1606d238c358680db0023d15e693d18d5927afd59c3f3961f4","nodeId":"4a29970c-82c3-4558-99ac-1b750cf66f5b","outcome":"completed","summary":"The review is complete. The findings file is written at `.imd-findings.json`, the snippets match the tree, and the scratch tests are removed (git status is clean apart from the gitignored findings file).\n\n**Outcome:** the vault's math holds up. I found two low-severity defects and no medium or higher issues in the Math Precision, Boundary and Numerical Gap areas.\n\n**Findings**\n\n- **Low, rate floor rounding.** The mid-period top-up check rebuilds the period total from floor-divided pieces, so a top-up of exactly the streamed amount lands one wei short and reverts with `RewardRateTooLow`. The README tells funders that exact amount is enough. Concrete input: stake 100 ether, fund 1000 ether, wait 3 days, fund 428571428571428571428. One wei more succeeds. The rounding only ever lowers the proposed rate, so the dangerous direction (dust slowing the stream) is still blocked. The suite misses this because its reward amount gives an exact 1e36 rate.\n- **Low, unbounded reward duration.** The constructor rejects zero but no upper bound. A duration of 1e40 makes every funding round to a zero rate and strands the tokens permanently, since there is no sweep. A duration near 2^256 makes funding revert forever. The launch value of 604800 is safe, so this is a deployment-input hazard.\n\n**What I verified as sound**\n\n- No overflow in any product: a 1-wei sole staker across nine maximal fundings keeps the accumulator near 1e45, and a later whale's `earned` stays far below 2^256.\n- Precision loss from per-block global checkpoints is dust: about 1e11 wei over 4,000 blocks on a 101M-token stake, and it stays in the vault.\n- Zero-rounding of per-second accrual needs sub-gwei fundings against the whole supply, so it is not material.\n- Period-end and lock-end comparisons are consistent at the exact boundary second.\n- The solvency invariant holds because every liability term rounds down and funding totals are bounded by what the vault received.\n\n**Coverage:** all 8 listed entry points have rows, plus the constructor and five invariant or boundary rows. Nothing was left unreached.","treeHash":null,"usage":{"cachedInputTokens":763177,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":27958,"runtime":"claude","turns":22,"wallClockMs":366768}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"aad1250d716d3f82","findings":[],"hash":"4a9ac26886462a69b6775fdc0cffe398c165a4faa195380fb430df8d95e9156a","nodeId":"0a1f31e3-3959-407f-836b-035b2d442499","outcome":"completed","summary":"Wrote [.imd-findings.json](/home/seat/.identitymd/work/ebe84da9-e370-493d-95c0-c593847dddb7/0a1f31e3-3959-407f-836b-035b2d442499/.imd-findings.json) with no substantiated defects and coverage for all 8 entry points.\n\nExisting unit/fuzz tests, all 8 invariants, and 7 additional permission checks passed. Source and configuration remained unchanged. Deployment-specific protected tests were inspected but not executed.","treeHash":null,"usage":{"cachedInputTokens":611072,"inputTokens":80867,"model":"gpt-6-astra","outputTokens":7334,"runtime":"codex","turns":5,"wallClockMs":270826}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c7630b22a73c1fb","findings":[{"citation":"resolved","description":"Merged from audit_math (low) and write_foundry_tests (info); same root cause. fundRewards rebuilds the period total from floor-divided pieces (line 167 `total += (currentRate * remaining) / PRECISION;`, and `undistributed`, itself a sum of floors) and then floors again at line 171 to get newRate. Whenever rewardRate is not an exact multiple of 1e18 per second (every funding except amounts like 604_800 ether), the recomputed total is 1 wei short of the real one, so newRate lands one PRECISION/rewardsDuration unit (~1.65e12) below currentRate and the call reverts with RewardRateTooLow. The README (lines 53-54, 122-123) tells funders a mid-period top-up must be 'at least what the current period has already streamed'; a funder sending exactly rewardRate*elapsed/1e18 is reverted. The same seam rejects fundRewards(0) mid-period while nobody is staked, where nothing has been streamed to anyone and the real total is unchanged. Rounding only ever lowers newRate, so the dangerous direction (a dust deposit slowing the stream) is still blocked. Impact: a spurious revert at the documented boundary plus gas; no funds at risk; state untouched. Minimal fix preserving the design: carry the rollover in rate units and divide once (e.g. `uint256 newRate = (received + undistributed) * PRECISION / rewardsDuration + (active ? currentRate * remaining / rewardsDuration : 0)`), or compare `newRate + 1 < currentRate`, or expose a ceiling-rounded `minimumTopUp()` view and correct the README to say 'streamed plus 2 wei'. Note test/StakingVault.edge.t.sol test_topUpOfExactStreamedAmountCanRevertByRounding pins the current revert and must be updated with the fix.","line":172,"path":"src/StakingVault.sol","reproduction":"Deploy StakingVault(token, 7 days) at t0 = 1_700_000_000. alice stakes 100 ether; funder calls fundRewards(1000 ether). rewardRate = 1e39/604800 = 1653439153439153439153439153439153. Warp to t0 + 3 days. streamed = rewardRate*259200/1e18 = 428571428571428571428; remainingRewards() = 571428571428571428571. Call fundRewards(428571428571428571428). Inside: total = 999999999999999999999 (1 wei short of 1000e18); newRate = total*1e18/604800 = 1653439153439153439151785714285714 < currentRate. Expected (per README): call succeeds and the period restarts at the same rate. Actual: revert RewardRateTooLow(1653439153439153439153439153439153, 1653439153439153439151785714285714). fundRewards(428571428571428571429) succeeds and restores rewardRate exactly. Second input: no stakers, fundRewards(1000 ether) at t0, warp t0 + 3 days, fundRewards(0): undistributed = 428571428571428571428, remaining = 571428571428571428571, same newRate, reverts although nothing was streamed to anyone. Verified with forge test on test/scratch/Repro.t.sol (test_exactStreamedTopUpReverts, test_zeroFundMidPeriodNobodyStaked), logged values above.","severity":"low","snippet":"        if (active && newRate < currentRate) revert RewardRateTooLow(currentRate, newRate);","title":"Mid-period rate floor floors twice, so a top-up of exactly the streamed amount (the README's stated minimum) and a zero-amount restart with nobody staked revert by 1 rate unit"},{"citation":"resolved","description":"Merged from audit_math and write_foundry_tests (both low); same root cause. The constructor (line 108 `if (rewardsDuration_ == 0) revert ZeroDuration();`) rejects only zero, and fundRewards rejects only `total == 0` (line 169), not `newRate == 0`. With rewardsDuration > total * 1e18 the division truncates to zero after _pull has already taken the tokens: rewardRate = 0, periodFinish = now + rewardsDuration, remainingRewards() = 0, undistributed stays 0 (it accumulates rate*elapsed), and no function can move the tokens out (no sweep by design). Every later funding below rewardsDuration/1e18 wei is swallowed the same way. With rewardsDuration_ near type(uint256).max, line 177 `periodFinish = block.timestamp + rewardsDuration;` overflows, so fundRewards always reverts and the vault can only take and return stake. The launch manifest uses 604800, where even a 1 wei funding yields rate 1e18/604800 > 0 (pinned by test_launchDurationNeverTruncatesRateToZero), so the launch deployment is not affected; this is a deployment-input hazard in the contract's own guards, and the README calls rewardsDuration 'the one deployment choice' with no bound. Minimal fix preserving the design: bound the constructor (e.g. revert if rewardsDuration_ > 365 days) and/or `if (newRate == 0) revert ZeroAmount();` in fundRewards so a funding that cannot stream is refused instead of absorbed.","line":171,"path":"src/StakingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {LaunchToken} from \"src/LaunchToken.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\n\n/// @notice Proof for the finding \"fundRewards accepts tokens at a reward rate of zero and strands them\".\n/// @dev Fails on the current code: the funding is pulled into the vault, `rewardRate` is 0, nothing is\n/// scheduled (`remainingRewards() == 0`), nothing is parked (`undistributed() == 0`) and nobody can ever\n/// earn or recover it. Passes once `fundRewards` rejects a zero rate (or the constructor bounds\n/// `rewardsDuration` so that `total * PRECISION / rewardsDuration` cannot truncate to zero).\ncontract ZeroRateFundingProofTest is Test {\n    uint256 internal constant START = 1_700_000_000;\n\n    function test_fundRewardsMustNotAcceptTokensAtZeroRate() public {\n        vm.warp(START);\n        LaunchToken token = new LaunchToken();\n        // Any duration above amount * 1e18 truncates the scaled rate to zero. 1e40 seconds does it\n        // for every funding below 1e22 wei (10,000 tokens).\n        StakingVault vault = new StakingVault(address(token), 1e40);\n        token.approve(address(vault), type(uint256).max);\n\n        address staker = makeAddr(\"staker\");\n        token.transfer(staker, 1 ether);\n        vm.startPrank(staker);\n        token.approve(address(vault), type(uint256).max);\n        vault.stake(1 ether);\n        vm.stopPrank();\n\n        uint256 funderBefore = token.balanceOf(address(this));\n        (bool ok,) = address(vault).call(abi.encodeCall(StakingVault.fundRewards, (1 ether)));\n\n        if (ok) {\n            // The call went through: then the funding must be streaming or parked, not lost.\n            assertEq(token.balanceOf(address(vault)), 2 ether, \"vault took the funding\");\n            assertEq(token.balanceOf(address(this)), funderBefore - 1 ether, \"funder paid\");\n            uint256 accountedFor = vault.remainingRewards() + vault.undistributed();\n            assertEq(accountedFor, 1 ether, \"funding accepted but neither scheduled nor parked: stranded\");\n            assertGt(vault.rewardRate(), 0, \"funding accepted at a zero reward rate\");\n            vm.warp(START + 365 days);\n            assertGt(vault.earned(staker), 0, \"sole staker earns nothing from an accepted funding\");\n        } else {\n            // A revert is the correct answer: nothing moved.\n            assertEq(token.balanceOf(address(this)), funderBefore);\n            assertEq(token.balanceOf(address(vault)), 1 ether);\n        }\n    }\n}","reproduction":"Input 1: deploy StakingVault(token, 1e40); alice stakes 100 ether; funder calls fundRewards(1 ether). Expected: revert (nothing moves) or the 1 ether is scheduled/parked and the sole staker eventually earns it. Actual: call succeeds; rewardRate() == 0, remainingRewards() == 0, undistributed() == 0, token.balanceOf(vault) == 101 ether; after warp +365 days earned(alice) == 0; a second fundRewards(1 ether) is absorbed the same way. Input 2: deploy StakingVault(token, type(uint256).max); fundRewards(1 ether) reverts with arithmetic overflow at periodFinish = block.timestamp + rewardsDuration, and so does every later call. Verified with forge test on test/scratch/Repro.t.sol (test_hugeDurationStrands, test_maxDurationBricks) and the attached proof (fails on current code: 'funding accepted but neither scheduled nor parked: stranded: 0 != 1000000000000000000').","severity":"low","snippet":"        uint256 newRate = (total * PRECISION) / rewardsDuration;","title":"rewardsDuration is unbounded: a duration above amount*1e18 makes fundRewards accept tokens at rewardRate 0 and strand them permanently; near-2^256 durations brick funding"}],"hash":"5a2fcb9d917f6d96bcc1796fdd10ddc1e1397cc4d73918c922e309754dac32fd","nodeId":"147443a6-4140-403f-9fe2-f9f1c820ccf3","outcome":"completed","summary":"The review is complete. The findings file is written, scratch tests are removed, and the working tree holds only `.imd-findings.json`.\n\n**Outcome:** the vault is sound on every fund-safety axis. All four specialist leads reproduce and collapse into two low findings, and my own pass found nothing beyond them.\n\n**Findings kept (both low):**\n- **Double-floor rate check in fundRewards.** A mid-period top-up of exactly the streamed amount, which the README names as the minimum, reverts with RewardRateTooLow by one rate unit. A zero-amount restart mid-period with nobody staked hits the same seam. Reproduced with the exact values the audit_math specialist gave (rate 1653439153439153439153439153439153, top-up of 428571428571428571428 reverts, plus 1 wei succeeds). No funds at risk. Merged from audit_math and write_foundry_tests.\n- **Unbounded rewardsDuration.** A duration above amount times 1e18 lets fundRewards pull tokens at a zero rate and strand them with no recovery path. A duration near 2^256 overflows periodFinish and bricks funding. The launch value of 604800 is safe, so this is a deployment-input hazard. The specialist's proof fails on current code for the stated reason and is attached. Merged from audit_math and write_foundry_tests.\n\n**Dropped or recalibrated:** nothing dropped. The write_foundry_tests \"info\" rating on the rounding issue is raised to low to match its duplicate.\n\n**Coverage:** all 8 entry points answered. Seven hold. fundRewards carries both findings. Three invariant rows added for balance backing, rate monotonicity, and principal custody.\n\n**Verification run:** the full suite passes with 88 tests. Both invariant suites and the edge suite already exercise non-round rates, so the test gap the audit_math specialist described is closed by the current tree.\n\n**Note for the fix:** the edge test that pins the exact-streamed-amount revert will need updating once finding 1 is resolved.","treeHash":null,"usage":{"cachedInputTokens":542920,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":21191,"runtime":"claude","turns":21,"wallClockMs":260877}},{"artifacts":[],"attempt":1,"bundleHash":"16defa7f0d176b1e145f3485afb832ba44b4377b736d4a52751b4263e92e6174","device":"468e82a89b9bfe18","findings":[],"hash":"b4a910c69d3d48e7206ae6312c06f83195af87a7d058d43306b8989181f8695e","nodeId":"b536b483-d15a-4810-82d6-ec5ab3e28e83","outcome":"completed","summary":"Done. The manifest is written at `launch.json` and is the only file I changed.\n\n**What it declares**\n\n- Token: LaunchToken, \"Stake Launch Token\" / \"STK\", 18 decimals, matching the name and symbol set in the source file.\n- One application contract: StakingVault with constructorArgs `[\"$token\", \"604800\"]`. The source constructor is `(address token_, uint256 rewardsDuration_)`, and the README fixes the launch reward period at 7 days. There is no owner argument because the vault has no privileged role.\n- Pool: native ETH as the paired currency, fee 3000, tickSpacing 60, the default sqrtPriceX96.\n- Notes describe the constructor, the no-owner design, the staking, lock, funding and claim behaviour, and that the factory supplies the LP, MerkleDistributor and guard.\n\n**Verification**\n\n- A schema check in Node confirmed all fields, patterns, limits, unique identifiers and backward references.\n- A scratch test deployed StakingVault with exactly the manifest's ABI encoding and confirmed the token wiring, the 7-day duration, the token metadata and that the constructor leaves the deployer's supply untouched.\n- Offline build and full test run passed: 57 tests, 0 failures.\n\nThe untracked dotfiles shown by git are sandbox device nodes, not edits. The scratch test lives under test/scratch and is discarded before submission.","treeHash":"7e5daf263b6b3d571467c2ce3c7ff8b0a89e953a","usage":{"cachedInputTokens":331566,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":4680,"runtime":"claude","turns":12,"wallClockMs":67699}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3a271480f26269e3","findings":[],"hash":"bd8771e9530d61fc25f4cb1aefe317128c1db005aeb01cbcf27532a12034514e","nodeId":"27590833-63b9-416c-90f9-71064f9de683","outcome":"completed","summary":"Saved [`.imd-findings.json`](/root/.identitymd/work/ebe84da9-e370-493d-95c0-c593847dddb7/27590833-63b9-416c-90f9-71064f9de683/.imd-findings.json) with no substantiated findings and coverage for all 8 entry points.\n\nOffline Foundry checks passed: 56 tests, including 8 invariants across 3,072 handler calls.\n\nThe protected launch harness was reviewed but not executed; deployment inputs were unavailable.","treeHash":null,"usage":{"cachedInputTokens":473472,"inputTokens":83065,"model":"gpt-6-astra","outputTokens":6231,"runtime":"codex","turns":4,"wallClockMs":216893}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"004eae350f695d24","findings":[],"hash":"c6fec66c5ed7564ac8ce3ff8c594e499e068ef3420ae070321df56af639117df","nodeId":"0e0f4e20-abbf-4e43-a70f-f7ad950f7f5f","outcome":"completed","summary":"Recorded no substantiated defects in [.imd-findings.json](.imd-findings.json), with coverage for all eight entry points.\n\nExisting and supplemental unit, fuzz, and invariant checks passed. Deployment-specific protected tests lacked required inputs and were not run. Production files remain unchanged.","treeHash":null,"usage":{"cachedInputTokens":753408,"inputTokens":82698,"model":"gpt-6-astra","outputTokens":8889,"runtime":"codex","turns":6,"wallClockMs":325313}},{"artifacts":[],"attempt":1,"bundleHash":"3590adec3dedf83715357769e82980047ce2b36e85d5155f64132df2131bfb02","device":"98b4506bef931d13","findings":[{"description":"fundRewards computes newRate = (total * PRECISION) / rewardsDuration and only rejects total == 0. When rewardsDuration > total * 1e18 the division truncates to zero: the tokens are pulled from the funder (the transfer has already happened in _pull), rewardRate becomes 0, periodFinish is set, and the amount is neither streamed (remainingRewards() == 0), nor parked (undistributed stays 0, because the undistributed accumulator is rate * elapsed), nor claimable by anyone. There is no sweep by design, so the funding is lost for good. The constructor accepts any non-zero rewardsDuration, so a vault deployed with a large duration silently eats every funding below rewardsDuration / 1e18 wei. With the launch value of 604800 seconds the rate cannot truncate for any amount >= 1 wei, so the launch deployment is not affected; the defect is in the contract's own guard, which should revert when newRate == 0 (or the constructor should bound rewardsDuration). The test suite pins the launch value as safe in test_launchDurationNeverTruncatesRateToZero and does not assert the stranding behaviour as correct.","line":171,"path":"src/StakingVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {LaunchToken} from \"src/LaunchToken.sol\";\nimport {StakingVault} from \"src/StakingVault.sol\";\n\n/// @notice Proof for the finding \"fundRewards accepts tokens at a reward rate of zero and strands them\".\n/// @dev Fails on the current code: the funding is pulled into the vault, `rewardRate` is 0, nothing is\n/// scheduled (`remainingRewards() == 0`), nothing is parked (`undistributed() == 0`) and nobody can ever\n/// earn or recover it. Passes once `fundRewards` rejects a zero rate (or the constructor bounds\n/// `rewardsDuration` so that `total * PRECISION / rewardsDuration` cannot truncate to zero).\ncontract ZeroRateFundingProofTest is Test {\n    uint256 internal constant START = 1_700_000_000;\n\n    function test_fundRewardsMustNotAcceptTokensAtZeroRate() public {\n        vm.warp(START);\n        LaunchToken token = new LaunchToken();\n        // Any duration above amount * 1e18 truncates the scaled rate to zero. 1e40 seconds does it\n        // for every funding below 1e22 wei (10,000 tokens).\n        StakingVault vault = new StakingVault(address(token), 1e40);\n        token.approve(address(vault), type(uint256).max);\n\n        address staker = makeAddr(\"staker\");\n        token.transfer(staker, 1 ether);\n        vm.startPrank(staker);\n        token.approve(address(vault), type(uint256).max);\n        vault.stake(1 ether);\n        vm.stopPrank();\n\n        uint256 funderBefore = token.balanceOf(address(this));\n        (bool ok,) = address(vault).call(abi.encodeCall(StakingVault.fundRewards, (1 ether)));\n\n        if (ok) {\n            // The call went through: then the funding must be streaming or parked, not lost.\n            assertEq(token.balanceOf(address(vault)), 2 ether, \"vault took the funding\");\n            assertEq(token.balanceOf(address(this)), funderBefore - 1 ether, \"funder paid\");\n            uint256 accountedFor = vault.remainingRewards() + vault.undistributed();\n            assertEq(accountedFor, 1 ether, \"funding accepted but neither scheduled nor parked: stranded\");\n            assertGt(vault.rewardRate(), 0, \"funding accepted at a zero reward rate\");\n            vm.warp(START + 365 days);\n            assertGt(vault.earned(staker), 0, \"sole staker earns nothing from an accepted funding\");\n        } else {\n            // A revert is the correct answer: nothing moved.\n            assertEq(token.balanceOf(address(this)), funderBefore);\n            assertEq(token.balanceOf(address(vault)), 1 ether);\n        }\n    }\n}","reproduction":"Deploy StakingVault(token, 1e40). A staker stakes 1 ether. A funder calls fundRewards(1 ether). Expected: the call reverts (nothing moves), or the 1 ether is scheduled/parked and the sole staker eventually earns it. Actual: the call succeeds, token.balanceOf(vault) == 2 ether, rewardRate() == 0, remainingRewards() == 0, undistributed() == 0, and earned(staker) == 0 after 365 days. Run: forge test --match-path test/scratch/ZeroRateFundingProof.t.sol (fails on the current code).","severity":"low","title":"fundRewards accepts tokens when the scaled reward rate truncates to zero, stranding them permanently"},{"description":"The README says a mid-period top-up 'has to be at least what the current period has already streamed'. Because the unstreamed remainder is floored once (currentRate * remaining / PRECISION) and the new rate is floored again (total * PRECISION / rewardsDuration), a top-up equal to floor(rate * elapsed / 1e18) can produce a new rate one unit below the current one and revert with RewardRateTooLow whenever the rate is not a whole multiple of 1e18 per second. Two extra wei always clear the floor. No funds are at risk and the revert leaves state untouched; this is a documentation/integration note so that funding tooling adds a margin. Pinned by test_topUpOfExactStreamedAmountCanRevertByRounding and testFuzz_minimumTopUpIsStreamedPlusTwoWei.","line":172,"path":"src/StakingVault.sol","reproduction":"Stake 100 ether; fundRewards(1 ether + 7) at t0 (rate = 1653439153439153450727513227513); warp t0 + 12345; streamed = rate * 12345 / 1e18. fundRewards(streamed) reverts RewardRateTooLow; fundRewards(streamed + 2) succeeds. Expected per README: fundRewards(streamed) succeeds. Actual: it reverts by 1 unit of rate.","severity":"info","title":"A mid-period top-up of exactly the streamed amount can be rejected by rounding; the README understates the minimum by up to 2 wei"}],"hash":"cdb8bd3084d37604df5a29cae9f5799813da61babcc4e733a1b8421fc5d22073","nodeId":"9218d248-c540-4542-a76b-5e76d6f373fc","outcome":"completed","summary":"Done. The suite passes from a clean offline build with no environment variables: 88 tests across 6 suites, 0 failures. The working tree is clean apart from the findings file, which the network's own exclude list keeps out of git by design.\n\n**What I added, on top of the existing 56 tests**\n\n- `test/StakingVault.edge.t.sol` holds 30 adversarial unit and fuzz tests (1000 fuzz runs via inline config). They cover rate-floor rounding at the boundary, funding one second before and exactly at period end, lock resets to the second, claims and partial withdrawals not touching the lock, a full unstake freezing rewards, whole-supply and one-wei positions, fifty consecutive top-ups, stake-and-exit round trips that must never profit, a 100% fee token, event amounts under a fee token, re-entrancy through sender and recipient token hooks on every entry point, twelve admin-style selectors that must not exist, ETH rejection, and donations never being paid to anyone.\n- `test/StakingVault.lifecycle.invariant.t.sol` is a second invariant harness with `fail_on_revert` on, 128 runs of depth 64. Six actors range from whales to a 500-wei account. The handler lands warps exactly on unlock times and period ends, performs full-balance operations, and makes adversarial calls that must revert. Twelve invariants check solvency, a bound on stranded dust, per-actor principal and token-balance accounting, lock enforcement, accumulator and clock monotonicity, the rate floor, and payout-equals-view. A fuzzed liveness test replays random sequences and then has everyone exit with exactly their principal.\n- `test/mocks/ReentrantToken.sol` is a hook-calling token for the re-entrancy tests.\n\n**Findings reported in `.imd-findings.json`**\n\n- Low, with a proof test: a vault built with a very large `rewardsDuration` accepts a funding whose scaled rate truncates to zero. The tokens are pulled in, nothing is streamed or parked, and no one can ever recover them. The launch value of 7 days cannot trigger this, and the suite pins that as safe rather than asserting the stranding is correct.\n- Info: a mid-period top-up of exactly the streamed amount can revert by rounding when the rate is not a whole multiple. Two extra wei always clear the floor, which the README should state.\n\n**Harness issues I caught and corrected before trusting results** were a prank consumed by a view call, a funder running out of balance in a fuzz, and a liabilities view that missed rewards streamed while nobody was staked before the next checkpoint. None turned out to be contract defects.","treeHash":"ec29539b6e4deeea0d84c846f8c336dbea7953c6","usage":{"cachedInputTokens":2979110,"inputTokens":866,"model":"claude-fable-5-1","outputTokens":69634,"runtime":"claude","turns":46,"wallClockMs":878368}},{"artifacts":[],"attempt":1,"bundleHash":"7bc464b32408ba35ada8c691a56d5591d37eb4ea5e108ee9ef031ce633521083","device":"98b4506bef931d13","findings":[],"hash":"e4e6f11ee9228830037e62fa195c619760bc8fa284743419a69ef80724444d4f","nodeId":"1067f4c0-b3a5-41f8-b08c-d4504492712e","outcome":"completed","summary":"Committed on `main` with a clean working tree. The staking vault and launch token are implemented, tested, and documented.\n\n**What was delivered**\n\n- `src/LaunchToken.sol`: fixed-supply ERC-20 \"Stake Launch Token\" (STK), 18 decimals, 10^27 minor units minted to the deployer, no constructor arguments, no mint, owner, pause, blocklist, fee or upgrade functions.\n- `src/StakingVault.sol`: ownerless vault. Stakers lock their whole position for 7 days from their latest stake. Anyone can fund rewards in the token. Rewards stream per second pro rata to stake using a Synthetix-style accumulator with the rate scaled by 1e18. Rewards streamed while nothing is staked are kept and re-streamed on the next funding. A mid-period top-up must not lower the current rate, which blocks dust-funding griefing. Deposits credit the amount actually received. Every external function is non-reentrant and follows checks-effects-interactions.\n- Tests: 56 in total. Unit and fuzz tests cover lock boundaries, pro-rata and per-second accrual, late joiners, funding rules, claim and exit, principal safety against non-stakers and admin-style selectors, direct transfers, and a fee-on-transfer mock. A handler-driven invariant suite over four actors checks that the vault balance always covers principal plus owed rewards plus the unstreamed remainder, that stakes sum to the total, that principal is conserved, and that claims never exceed funding.\n- `script/Deploy.s.sol` with a config-taking `deploy` function that the tests call directly.\n- `README.md` documents behaviour, assumptions, the one deployment parameter (`rewardsDuration`, 604800 at launch, with constructor arguments `[\"$token\", \"604800\"]`), and operational responsibilities.\n- `lib/` vendors forge-std 1.16.2 and only the OpenZeppelin 5.7.0 files the contracts import, as ordinary files with no submodules.\n\n**Verification**\n\nFrom a clean build with an empty environment and `--offline`, `forge build`, `forge test` and `forge fmt --check` all pass. A scratch replica of the protected floor confirmed both runtimes deploy from a CREATE2 probe without moving the supply, stay under the size limit, and contain no DELEGATECALL, CALLCODE or SELFDESTRUCT. A longer invariant campaign of 256 runs at depth 100 also passed. Slither and Mythril are not available here and were not run.\n\n**Design choices to be aware of**\n\n- Each new stake resets the 7-day lock on the whole position. Rewards themselves are never locked.\n- Tokens sent to the vault by plain transfer are stranded by design, since there is no sweep role.\n- The commit uses your email for the author identity because the repository had none configured.","treeHash":"a7d3b8afff2839646783f7b99dc5e367153e04e3","usage":{"cachedInputTokens":1396321,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":46939,"runtime":"claude","turns":38,"wallClockMs":542554}}],"verification":[{"checks":[{"durationMs":2179,"exitCode":0,"name":"build","output":"Compiling 41 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.93s\nCompiler run successful!\n","passed":true},{"durationMs":1039,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployRejectsZeroDuration() (gas: 2253110)\n[PASS] test_deployWiresVaultToToken() (gas: 2980424)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 12.07ms (11.33ms CPU time)\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29173)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 32085)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 257858)\n[PASS] test_transferMovesExactAmount() (gas: 83621)\n[PASS] test_transferRevertsWhenBalanceInsufficient() (gas: 36545)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 13.96ms (1.68ms CPU time)\n\nRan 48 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] testFuzz_earnedNeverExceedsFundedAndRewardsAreProRata(uint256,uint256,uint256,uint256) (runs: 256, μ: 449504, ~: 449901)\n[PASS] testFuzz_lockBoundary(uint256) (runs: 256, μ: 198352, ~: 186940)\n[PASS] testFuzz_withdrawReturnsExactlyWhatWasStaked(uint256,uint256) (runs: 256, μ: 446066, ~: 445034)\n[PASS] test_anyoneCanFund() (gas: 155546)\n[PASS] test_claimPaysAndResets() (gas: 472950)\n[PASS] test_claimTwiceDoesNotPayTwice() (gas: 448436)\n[PASS] test_claimWhileLockedIsAllowed() (gas: 389765)\n[PASS] test_claimWithNothingEarnedIsNoop() (gas: 67828)\n[PASS] test_constructorRejectsZeroDuration() (gas: 6082)\n[PASS] test_constructorRejectsZeroToken() (gas: 3999)\n[PASS] test_constructorSetsImmutables() (gas: 40687)\n[PASS] test_directTransferDoesNotChangeAccounting() (gas: 228029)\n[PASS] test_exitReturnsPrincipalAndRewards() (gas: 445751)\n[PASS] test_exitWhileLockedReverts() (gas: 309767)\n[PASS] test_feeOnTransferTokenIsCreditedAtReceivedAmount() (gas: 1098329)\n[PASS] test_fundAfterPeriodEndsStartsFreshPeriod() (gas: 406269)\n[PASS] test_fundRewardsStartsPeriod() (gas: 185345)\n[PASS] test_fundWithoutApprovalReverts() (gas: 111320)\n[PASS] test_fundZeroDuringPeriodCannotSlowStream() (gas: 344236)\n[PASS] test_fundZeroWithNothingPendingReverts() (gas: 48474)\n[PASS] test_fundingDoesNotCreditStakeOrPrincipal() (gas: 197410)\n[PASS] test_lateStakerOnlyEarnsFromJoining() (gas: 485003)\n[PASS] test_noAdminEntryPointsCanTouchPrincipal() (gas: 363766)\n[PASS] test_noRewardsBeforeFunding() (gas: 188208)\n[PASS] test_nonStakerCannotWithdrawOthersPrincipal() (gas: 218819)\n[PASS] test_partialWithdrawKeepsRemainderStaked() (gas: 306032)\n[PASS] test_rewardPayoutsNeverDipIntoPrincipal() (gas: 755830)\n[PASS] test_rewardTokenIsStakingToken() (gas: 20798)\n[PASS] test_rewardsKeepAccruingAfterClaim() (gas: 403903)\n[PASS] test_rewardsSplitProRataToStake() (gas: 442121)\n[PASS] test_rewardsStreamPerSecond() (gas: 327252)\n[PASS] test_rewardsWhileNobodyStakedRollIntoNextFunding() (gas: 447542)\n[PASS] test_secondStakeResetsLockForWholePosition() (gas: 287166)\n[PASS] test_singleStakerEarnsWholeStream() (gas: 356471)\n[PASS] test_stakeMoreThanBalanceReverts() (gas: 74525)\n[PASS] test_stakeMovesTokensAndLocksForSevenDays() (gas: 208915)\n[PASS] test_stakeOnlyAffectsCaller() (gas: 289897)\n[PASS] test_stakeWithoutApprovalReverts() (gas: 120969)\n[PASS] test_stakeZeroReverts() (gas: 37380)\n[PASS] test_topUpBelowCurrentRateReverts() (gas: 373533)\n[PASS] test_topUpMidPeriodRollsRemainderAndExtends() (gas: 416243)\n[PASS] test_undistributedTrackedWhenEveryoneExits() (gas: 675335)\n[PASS] test_withdrawAtExactUnlockSucceeds() (gas: 246365)\n[PASS] test_withdrawBeforeUnlockReverts() (gas: 185932)\n[PASS] test_withdrawMoreThanStakedReverts() (gas: 183583)\n[PASS] test_withdrawZeroReverts() (gas: 181010)\n[PASS] test_withdrawnPrincipalStopsEarning() (gas: 636509)\n[PASS] test_zeroAmountFundRestartsLeftoversWhenIdle() (gas: 373534)\nSuite result: ok. 48 passed; 0 failed; 0 skipped; finished in 36.31ms (74.64ms CPU time)\n\nRan 1 test for test/StakingVault.invariant.t.sol:StakingVaultInvariantTest\n[PASS]\nStakingVaultInvariantTest invariants:\n[PASS] invariant_balanceCoversAllLiabilities\n[PASS] invariant_balanceCoversPrincipal\n[PASS] invariant_balanceCoversPrincipalAndOwedRewards\n[PASS] invariant_claimedNeverExceedsFunded\n[PASS] invariant_principalConserved\n[PASS] invariant_remainingRewardsBoundedByPeriod\n[PASS] invariant_totalStakedMatchesAccounts\n[PASS] invariant_unlockWithinLockPeriod\n StakingVaultInvariantTest invariants (runs: 64, calls: 3072, reverts: 0)\n\n╭---------------------+----------+-------+---------+----------╮\n| Contract            | Selector | Calls | Reverts | Discards |\n+=============================================================+\n| StakingVaultHandler | claim    | 409   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | donate   | 459   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | exit     | 423   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | fund     | 417   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | stake    | 456   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | warp     | 454   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | withdraw | 454   | 0       | 0        |\n╰---------------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 904.34ms (886.08ms CPU time)\n\nRan 4 test suites in 915.43ms (966.68ms CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":64,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.fundRewards(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.withdraw(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":148,\"foundry.toml\":44,\"launch.json\":22,\"remappings.txt\":2,\"script/Deploy.s.sol\":35,\"src/LaunchToken.sol\":19,\"src/StakingVault.sol\":267,\"test/Deploy.t.sol\":27,\"test/LaunchToken.t.sol\":65,\"test/StakingVault.invariant.t.sol\":194,\"test/StakingVault.t.sol\":632,\"test/mocks/FeeOnTransferToken.sol\":26},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"b4a910c69d3d48e7206ae6312c06f83195af87a7d058d43306b8989181f8695e","verifiedTreeHash":"7e5daf263b6b3d571467c2ce3c7ff8b0a89e953a","verifierVersion":"0.1.0+9f8ef295"},{"checks":[{"durationMs":4696,"exitCode":0,"name":"build","output":"Compiling 44 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.52s\nCompiler run successful!\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/StakingVault.edge.t.sol:465:54\n    │\n463 │             vm.warp(START + i * 1 hours);\n    │             ──────────────────────────── `vm.warp` changes this environment here\n464 │             // Each top-up must clear the floor: streamed so far in this period plus a margin.\n465 │             uint256 streamed = vault.rewardRate() * (block.timestamp - vault.lastUpdateTime()) / PRECISION;\n    │                                                      ━━━━━━━━━━━━━━━\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/StakingVault.edge.t.sol:489:21\n    │\n489 │             vm.warp(block.timestamp + LOCK);\n    │             ────────━━━━━━━━━━━━━━━──────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n    ╭▸ test/StakingVault.edge.t.sol:507:21\n    │\n507 │             vm.warp(block.timestamp + LOCK);\n    │             ────────━━━━━━━━━━━━━━━──────── `vm.warp` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockTimestamp()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":7887,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployRejectsZeroDuration() (gas: 2253110)\n[PASS] test_deployWiresVaultToToken() (gas: 2980424)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 784.47µs (832.56µs CPU time)\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29173)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 32085)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 257858)\n[PASS] test_transferMovesExactAmount() (gas: 83621)\n[PASS] test_transferRevertsWhenBalanceInsufficient() (gas: 36545)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 4.30ms (1.02ms CPU time)\n\nRan 48 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] testFuzz_earnedNeverExceedsFundedAndRewardsAreProRata(uint256,uint256,uint256,uint256) (runs: 256, μ: 449597, ~: 449888)\n[PASS] testFuzz_lockBoundary(uint256) (runs: 256, μ: 196737, ~: 186940)\n[PASS] testFuzz_withdrawReturnsExactlyWhatWasStaked(uint256,uint256) (runs: 256, μ: 445747, ~: 445034)\n[PASS] test_anyoneCanFund() (gas: 155546)\n[PASS] test_claimPaysAndResets() (gas: 472950)\n[PASS] test_claimTwiceDoesNotPayTwice() (gas: 448436)\n[PASS] test_claimWhileLockedIsAllowed() (gas: 389765)\n[PASS] test_claimWithNothingEarnedIsNoop() (gas: 67828)\n[PASS] test_constructorRejectsZeroDuration() (gas: 6082)\n[PASS] test_constructorRejectsZeroToken() (gas: 3999)\n[PASS] test_constructorSetsImmutables() (gas: 40687)\n[PASS] test_directTransferDoesNotChangeAccounting() (gas: 228029)\n[PASS] test_exitReturnsPrincipalAndRewards() (gas: 445751)\n[PASS] test_exitWhileLockedReverts() (gas: 309767)\n[PASS] test_feeOnTransferTokenIsCreditedAtReceivedAmount() (gas: 1098329)\n[PASS] test_fundAfterPeriodEndsStartsFreshPeriod() (gas: 406269)\n[PASS] test_fundRewardsStartsPeriod() (gas: 185345)\n[PASS] test_fundWithoutApprovalReverts() (gas: 111320)\n[PASS] test_fundZeroDuringPeriodCannotSlowStream() (gas: 344236)\n[PASS] test_fundZeroWithNothingPendingReverts() (gas: 48474)\n[PASS] test_fundingDoesNotCreditStakeOrPrincipal() (gas: 197410)\n[PASS] test_lateStakerOnlyEarnsFromJoining() (gas: 485003)\n[PASS] test_noAdminEntryPointsCanTouchPrincipal() (gas: 363766)\n[PASS] test_noRewardsBeforeFunding() (gas: 188208)\n[PASS] test_nonStakerCannotWithdrawOthersPrincipal() (gas: 218819)\n[PASS] test_partialWithdrawKeepsRemainderStaked() (gas: 306032)\n[PASS] test_rewardPayoutsNeverDipIntoPrincipal() (gas: 755830)\n[PASS] test_rewardTokenIsStakingToken() (gas: 20798)\n[PASS] test_rewardsKeepAccruingAfterClaim() (gas: 403903)\n[PASS] test_rewardsSplitProRataToStake() (gas: 442121)\n[PASS] test_rewardsStreamPerSecond() (gas: 327252)\n[PASS] test_rewardsWhileNobodyStakedRollIntoNextFunding() (gas: 447542)\n[PASS] test_secondStakeResetsLockForWholePosition() (gas: 287166)\n[PASS] test_singleStakerEarnsWholeStream() (gas: 356471)\n[PASS] test_stakeMoreThanBalanceReverts() (gas: 74525)\n[PASS] test_stakeMovesTokensAndLocksForSevenDays() (gas: 208915)\n[PASS] test_stakeOnlyAffectsCaller() (gas: 289897)\n[PASS] test_stakeWithoutApprovalReverts() (gas: 120969)\n[PASS] test_stakeZeroReverts() (gas: 37380)\n[PASS] test_topUpBelowCurrentRateReverts() (gas: 373533)\n[PASS] test_topUpMidPeriodRollsRemainderAndExtends() (gas: 416243)\n[PASS] test_undistributedTrackedWhenEveryoneExits() (gas: 675335)\n[PASS] test_withdrawAtExactUnlockSucceeds() (gas: 246365)\n[PASS] test_withdrawBeforeUnlockReverts() (gas: 185932)\n[PASS] test_withdrawMoreThanStakedReverts() (gas: 183583)\n[PASS] test_withdrawZeroReverts() (gas: 181010)\n[PASS] test_withdrawnPrincipalStopsEarning() (gas: 636509)\n[PASS] test_zeroAmountFundRestartsLeftoversWhenIdle() (gas: 373534)\nSuite result: ok. 48 passed; 0 failed; 0 skipped; finished in 194.70ms (433.41ms CPU time)\n\nRan 30 tests for test/StakingVault.edge.t.sol:StakingVaultEdgeTest\n[PASS] testFuzz_lockIsExactlySevenDaysFromLatestStake(uint256,uint256) (runs: 1000, μ: 359351, ~: 359428)\n[PASS] testFuzz_minimumTopUpIsStreamedPlusTwoWei(uint256,uint256,uint256) (runs: 1000, μ: 534170, ~: 546490)\n[PASS] testFuzz_rewardsConservedAcrossTwoPeriodsWithChurn(uint256,uint256,uint256,uint256,uint256) (runs: 1000, μ: 701092, ~: 707504)\n[PASS] testFuzz_stakeWithdrawCyclesNeverProfitWithoutRewards(uint256,uint8) (runs: 1000, μ: 1075775, ~: 851719)\n[PASS] testFuzz_stakeWithdrawCyclesWithRewardsNeverExceedFunding(uint256,uint256,uint8) (runs: 1000, μ: 763504, ~: 776026)\n[PASS] test_claimByNonStakerAfterOthersEarnPaysNothing() (gas: 399381)\n[PASS] test_claimDoesNotResetLock() (gas: 484183)\n[PASS] test_donationIsNeverPaidToAnyone() (gas: 558135)\n[PASS] test_dustFundingAgainstHugeStakeLosesAtMostRoundingWei() (gas: 316390)\n[PASS] test_eventsReportReceivedNotRequested() (gas: 960741)\n[PASS] test_exitInStakeBlockReverts() (gas: 312282)\n[PASS] test_fiftyTopUpsAccumulateIntoOneClaimableStream() (gas: 5055827)\n[PASS] test_fullUnstakeKeepsAccruedRewardsAndStopsFurtherAccrual() (gas: 793308)\n[PASS] test_fundAtExactPeriodFinishStartsFreshPeriodAtAnyRate() (gas: 463131)\n[PASS] test_fundOneSecondBeforeFinishStillEnforcesFloor() (gas: 367627)\n[PASS] test_launchDurationNeverTruncatesRateToZero() (gas: 314918)\n[PASS] test_noEntryPointActsOnAnotherAccountsPosition() (gas: 623741)\n[PASS] test_oneWeiStakerAloneEarnsWholeStream() (gas: 427745)\n[PASS] test_oneWeiStakerBesideWhaleEarnsNothingButCanStillExit() (gas: 580667)\n[PASS] test_reentrancyFromTokenHooksIsBlockedOnEveryEntryPoint() (gas: 2421799)\n[PASS] test_restakeAfterUnlockRelocksWholeRemainder() (gas: 381705)\n[PASS] test_rewardPerTokenNeverDecreasesAndFreezesAfterFinish() (gas: 1084581)\n[PASS] test_sameSecondRefundIsHarmlessRestart() (gas: 427555)\n[PASS] test_stakeTwiceInSameBlockSumsAndLocksOnce() (gas: 248311)\n[PASS] test_tokenThatDeliversNothingIsRejected() (gas: 812814)\n[PASS] test_topUpOfExactStreamedAmountCanRevertByRounding() (gas: 495384)\n[PASS] test_undistributedCannotBeRestartedMidPeriodButCanAtFinish() (gas: 520583)\n[PASS] test_vaultRejectsEther() (gas: 67601)\n[PASS] test_wholeSupplyMinusRewardsCanBeStakedAndReturned() (gas: 859545)\n[PASS] test_withdrawDoesNotResetLockForRemainder() (gas: 298790)\nSuite result: ok. 30 passed; 0 failed; 0 skipped; finished in 305.42ms (1.31s CPU time)\n\nRan 1 test for test/StakingVault.invariant.t.sol:StakingVaultInvariantTest\n[PASS]\nStakingVaultInvariantTest invariants:\n[PASS] invariant_balanceCoversAllLiabilities\n[PASS] invariant_balanceCoversPrincipal\n[PASS] invariant_balanceCoversPrincipalAndOwedRewards\n[PASS] invariant_claimedNeverExceedsFunded\n[PASS] invariant_principalConserved\n[PASS] invariant_remainingRewardsBoundedByPeriod\n[PASS] invariant_totalStakedMatchesAccounts\n[PASS] invariant_unlockWithinLockPeriod\n StakingVaultInvariantTest invariants (runs: 64, calls: 3072, reverts: 0)\n\n╭---------------------+----------+-------+---------+----------╮\n| Contract            | Selector | Calls | Reverts | Discards |\n+=============================================================+\n| StakingVaultHandler | claim    | 467   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | donate   | 430   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | exit     | 449   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | fund     | 427   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | stake    | 403   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | warp     | 452   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | withdraw | 444   | 0       | 0        |\n╰---------------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.29s (1.28s CPU time)\n\nRan 2 tests for test/StakingVault.lifecycle.invariant.t.sol:StakingVaultLifecycleInvariantTest\n[PASS]\nStakingVaultLifecycleInvariantTest invariants:\n[PASS] invariant_adversarialCallsAlwaysRevert\n[PASS] invariant_balanceCoversAllLiabilities\n[PASS] invariant_lockIsNeverBypassed\n[PASS] invariant_payoutsMatchViews\n[PASS] invariant_perActorPrincipalConserved\n[PASS] invariant_perActorTokenBalanceExplained\n[PASS] invariant_rateNeverLoweredWhileActive\n[PASS] invariant_rewardPerTokenMonotonic\n[PASS] invariant_rewardsPaidAndOwedNeverExceedFunded\n[PASS] invariant_strandedValueIsOnlyDonationsAndRoundingDust\n[PASS] invariant_streamClockMonotonic\n[PASS] invariant_totalStakedMatchesAccountsAndGhosts\n StakingVaultLifecycleInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭------------------+----------------------+-------+---------+----------╮\n| Contract         | Selector             | Calls | Reverts | Discards |\n+======================================================================+\n| LifecycleHandler | attemptEarlyWithdraw | 667   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | attemptOverWithdraw  | 621   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | attemptSlowStream    | 634   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | claim                | 617   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | donate               | 661   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | exit                 | 653   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | fund                 | 642   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | fundZero             | 633   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | stake                | 640   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | warp                 | 563   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | warpToPeriodFinish   | 646   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | warpToUnlock         | 585   | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | withdraw             | 630   | 0       | 0        |\n╰------------------+----------------------+-------+---------+----------╯\n\n[PASS] testFuzz_afterAnySequenceEveryoneCanExitWithExactPrincipal(uint256,uint8) (runs: 200, μ: 3085542, ~: 2455593)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 7.75s (7.95s CPU time)\n\nRan 6 test suites in 7.76s (9.55s CPU time): 88 tests passed, 0 failed, 0 skipped (88 total tests)\n","passed":true},{"durationMs":60,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.fundRewards(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.withdraw(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":148,\"foundry.toml\":44,\"remappings.txt\":2,\"script/Deploy.s.sol\":35,\"src/LaunchToken.sol\":19,\"src/StakingVault.sol\":267,\"test/Deploy.t.sol\":27,\"test/LaunchToken.t.sol\":65,\"test/StakingVault.edge.t.sol\":689,\"test/StakingVault.invariant.t.sol\":194,\"test/StakingVault.lifecycle.invariant.t.sol\":522,\"test/StakingVault.t.sol\":632,\"test/mocks/FeeOnTransferToken.sol\":26,\"test/mocks/ReentrantToken.sol\":30},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"cdb8bd3084d37604df5a29cae9f5799813da61babcc4e733a1b8421fc5d22073","verifiedTreeHash":"ec29539b6e4deeea0d84c846f8c336dbea7953c6","verifierVersion":"0.1.0+9f8ef295"},{"checks":[{"durationMs":1579,"exitCode":0,"name":"build","output":"Compiling 41 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.46s\nCompiler run successful!\n","passed":true},{"durationMs":806,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/Deploy.t.sol:DeployTest\n[PASS] test_deployRejectsZeroDuration() (gas: 2253110)\n[PASS] test_deployWiresVaultToToken() (gas: 2980424)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 556.48µs (315.21µs CPU time)\n\nRan 5 tests for test/LaunchToken.t.sol:LaunchTokenTest\n[PASS] test_metadata() (gas: 29173)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 32085)\n[PASS] test_noMintOrAdminEntryPoints() (gas: 257858)\n[PASS] test_transferMovesExactAmount() (gas: 83621)\n[PASS] test_transferRevertsWhenBalanceInsufficient() (gas: 36545)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 13.85ms (1.00ms CPU time)\n\nRan 48 tests for test/StakingVault.t.sol:StakingVaultTest\n[PASS] testFuzz_earnedNeverExceedsFundedAndRewardsAreProRata(uint256,uint256,uint256,uint256) (runs: 256, μ: 449449, ~: 449870)\n[PASS] testFuzz_lockBoundary(uint256) (runs: 256, μ: 199163, ~: 186940)\n[PASS] testFuzz_withdrawReturnsExactlyWhatWasStaked(uint256,uint256) (runs: 256, μ: 445824, ~: 445058)\n[PASS] test_anyoneCanFund() (gas: 155546)\n[PASS] test_claimPaysAndResets() (gas: 472950)\n[PASS] test_claimTwiceDoesNotPayTwice() (gas: 448436)\n[PASS] test_claimWhileLockedIsAllowed() (gas: 389765)\n[PASS] test_claimWithNothingEarnedIsNoop() (gas: 67828)\n[PASS] test_constructorRejectsZeroDuration() (gas: 6082)\n[PASS] test_constructorRejectsZeroToken() (gas: 3999)\n[PASS] test_constructorSetsImmutables() (gas: 40687)\n[PASS] test_directTransferDoesNotChangeAccounting() (gas: 228029)\n[PASS] test_exitReturnsPrincipalAndRewards() (gas: 445751)\n[PASS] test_exitWhileLockedReverts() (gas: 309767)\n[PASS] test_feeOnTransferTokenIsCreditedAtReceivedAmount() (gas: 1098329)\n[PASS] test_fundAfterPeriodEndsStartsFreshPeriod() (gas: 406269)\n[PASS] test_fundRewardsStartsPeriod() (gas: 185345)\n[PASS] test_fundWithoutApprovalReverts() (gas: 111320)\n[PASS] test_fundZeroDuringPeriodCannotSlowStream() (gas: 344236)\n[PASS] test_fundZeroWithNothingPendingReverts() (gas: 48474)\n[PASS] test_fundingDoesNotCreditStakeOrPrincipal() (gas: 197410)\n[PASS] test_lateStakerOnlyEarnsFromJoining() (gas: 485003)\n[PASS] test_noAdminEntryPointsCanTouchPrincipal() (gas: 363766)\n[PASS] test_noRewardsBeforeFunding() (gas: 188208)\n[PASS] test_nonStakerCannotWithdrawOthersPrincipal() (gas: 218819)\n[PASS] test_partialWithdrawKeepsRemainderStaked() (gas: 306032)\n[PASS] test_rewardPayoutsNeverDipIntoPrincipal() (gas: 755830)\n[PASS] test_rewardTokenIsStakingToken() (gas: 20798)\n[PASS] test_rewardsKeepAccruingAfterClaim() (gas: 403903)\n[PASS] test_rewardsSplitProRataToStake() (gas: 442121)\n[PASS] test_rewardsStreamPerSecond() (gas: 327252)\n[PASS] test_rewardsWhileNobodyStakedRollIntoNextFunding() (gas: 447542)\n[PASS] test_secondStakeResetsLockForWholePosition() (gas: 287166)\n[PASS] test_singleStakerEarnsWholeStream() (gas: 356471)\n[PASS] test_stakeMoreThanBalanceReverts() (gas: 74525)\n[PASS] test_stakeMovesTokensAndLocksForSevenDays() (gas: 208915)\n[PASS] test_stakeOnlyAffectsCaller() (gas: 289897)\n[PASS] test_stakeWithoutApprovalReverts() (gas: 120969)\n[PASS] test_stakeZeroReverts() (gas: 37380)\n[PASS] test_topUpBelowCurrentRateReverts() (gas: 373533)\n[PASS] test_topUpMidPeriodRollsRemainderAndExtends() (gas: 416243)\n[PASS] test_undistributedTrackedWhenEveryoneExits() (gas: 675335)\n[PASS] test_withdrawAtExactUnlockSucceeds() (gas: 246365)\n[PASS] test_withdrawBeforeUnlockReverts() (gas: 185932)\n[PASS] test_withdrawMoreThanStakedReverts() (gas: 183583)\n[PASS] test_withdrawZeroReverts() (gas: 181010)\n[PASS] test_withdrawnPrincipalStopsEarning() (gas: 636509)\n[PASS] test_zeroAmountFundRestartsLeftoversWhenIdle() (gas: 373534)\nSuite result: ok. 48 passed; 0 failed; 0 skipped; finished in 14.33ms (51.56ms CPU time)\n\nRan 1 test for test/StakingVault.invariant.t.sol:StakingVaultInvariantTest\n[PASS]\nStakingVaultInvariantTest invariants:\n[PASS] invariant_balanceCoversAllLiabilities\n[PASS] invariant_balanceCoversPrincipal\n[PASS] invariant_balanceCoversPrincipalAndOwedRewards\n[PASS] invariant_claimedNeverExceedsFunded\n[PASS] invariant_principalConserved\n[PASS] invariant_remainingRewardsBoundedByPeriod\n[PASS] invariant_totalStakedMatchesAccounts\n[PASS] invariant_unlockWithinLockPeriod\n StakingVaultInvariantTest invariants (runs: 64, calls: 3072, reverts: 0)\n\n╭---------------------+----------+-------+---------+----------╮\n| Contract            | Selector | Calls | Reverts | Discards |\n+=============================================================+\n| StakingVaultHandler | claim    | 421   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | donate   | 440   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | exit     | 482   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | fund     | 414   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | stake    | 432   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | warp     | 452   | 0       | 0        |\n|---------------------+----------+-------+---------+----------|\n| StakingVaultHandler | withdraw | 431   | 0       | 0        |\n╰---------------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 718.91ms (716.54ms CPU time)\n\nRan 4 test suites in 720.72ms (747.64ms CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":46,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LaunchToken.approve(address,uint256)\",\"LaunchToken.transfer(address,uint256)\",\"LaunchToken.transferFrom(address,address,uint256)\",\"StakingVault.claim()\",\"StakingVault.exit()\",\"StakingVault.fundRewards(uint256)\",\"StakingVault.stake(uint256)\",\"StakingVault.withdraw(uint256)\"],\"files\":{\".gitignore\":4,\"README.md\":148,\"foundry.toml\":44,\"remappings.txt\":2,\"script/Deploy.s.sol\":35,\"src/LaunchToken.sol\":19,\"src/StakingVault.sol\":267,\"test/Deploy.t.sol\":27,\"test/LaunchToken.t.sol\":65,\"test/StakingVault.invariant.t.sol\":194,\"test/StakingVault.t.sol\":632,\"test/mocks/FeeOnTransferToken.sol\":26},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":731,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/StakingVault.sol:192: StakingVault.rewardPerToken() (src/StakingVault.sol#192-196) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/StakingVault.sol:230: StakingVault._claim() (src/StakingVault.sol#230-238) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/StakingVault.sol:261: StakingVault._pull(uint256) (src/StakingVault.sol#261-266) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/StakingVault.sol:158: StakingVault.fundRewards(uint256) (src/StakingVault.sol#158-180) uses a dangerous strict equality:\n[low/medium] timestamp at src/StakingVault.sol:206: StakingVault.remainingRewards() (src/StakingVault.sol#206-209) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:215: StakingVault._withdraw(uint256) (src/StakingVault.sol#215-228) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:242: StakingVault._updateReward(address) (src/StakingVault.sol#242-257) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:158: StakingVault.fundRewards(uint256) (src/StakingVault.sol#158-180) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:230: StakingVault._claim() (src/StakingVault.sol#230-238) uses timestamp for comparisons\n[low/medium] timestamp at src/StakingVault.sol:187: StakingVault.lastTimeRewardApplicable() (src/StakingVault.sol#187-189) uses timestamp for comparisons","passed":true},{"durationMs":308,"exitCode":0,"name":"aderyn","output":"[low] large-numeric-literal at src/LaunchToken.sol:14: Large Numeric Literal","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e4e6f11ee9228830037e62fa195c619760bc8fa284743419a69ef80724444d4f","verifiedTreeHash":"a7d3b8afff2839646783f7b99dc5e367153e04e3","verifierVersion":"0.1.0+9f8ef295"}]}