{"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":"796405ca-1aef-4b22-945a-61cadefb4cb4","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"9259e52e222c21433affb416cfcdf87f9016f1b35934c54d0d4c6403de73d4ac","dependsOn":["build_contract_project","write_foundry_tests","manifest"],"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":"b8d43fb0e363ef45e7979aceb7dbe24eb31a7071f95fab20bb7bb37169583fe0","dependsOn":["build_contract_project","write_foundry_tests","manifest"],"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":"fd8bbf1d1a0ad0195ef5a175c489cbcbbd1481f2132cec74cfb511938698754d","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":"waiting"},{"acceptedSubmissionHash":"1a74b63773d442360d7076da0ea1c3c86cf2669c894528d6f13c18d9eec43a8b","dependsOn":["build_contract_project","write_foundry_tests","manifest"],"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":"a9956308d7ef54cbd4fc1ec5d084d6e185119d35c9e8edf1b6f3b9eefc424736","dependsOn":["build_contract_project","write_foundry_tests","manifest"],"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":"a60ca1db144ba1fa0a8bceb926e9ab23d09ed780fcce72a39f645fa922cca065","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":"283c30d79d3985329be180c1715a6ca9cd227cc79f7eea27ead3cf21125a469a","dependsOn":["build_contract_project","write_foundry_tests"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"failed"},{"acceptedSubmissionHash":"948a45d9275599eecc133bda5de3a7e2e1c0b368756c29aa820087ec41e441cf","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":"SIMD Vault on Robinhood Chain: the contract that pays for and owns the fees of the \"Test\" token launch, and splits everything it receives every 7 days. Three linked contracts, no owner, no admin, no upgrade, no pause.\r\n\r\nFixed addresses (Robinhood Chain):\r\n- IMD token: 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127\r\n- Identity.md Intake: 0xa43e6f75ee006411f79ac1c84120606c2330de82 (request(bytes32 action, bytes body, (address target, bytes4 selector) callback, address asset, uint256 amount); price from priceOf(action, asset))\r\n- Identity.md launch factory: 0x9c9d2fcb75c2c132c0ac0c42df3819a42347265e (claimFees(uint64 launchNumber) can be called by anyone and pays the launch's requester; positionOf(uint64) returns (PoolKey key, int24 tickLower, int24 tickUpper, address requester))\r\n- Dev wallet: 0xdfa1f1f6feaa04f7874eff0b7754b7ffaebd8bc0\r\n- Marketing wallet: 0xdfa1f1f6feaa04f7874eff0b7754b7ffaebd8bc0\r\n\r\n1. SIMDVault\r\n   - launch(bytes body): callable only by the dev wallet, only once. Approves Intake for exactly priceOf(\"launch.open@launch-1\", IMD) and calls Intake.request(\"launch.open@launch-1\", body, no callback, IMD, that price), paid from the vault's own IMD. This makes the vault the launch's requester, so the factory pays it the launch's creator fees for good.\r\n   - bindLaunch(uint64 launchNumber, address token): callable only by the dev wallet, only once, and only if factory.positionOf(launchNumber).requester == address(this). Stores both for good and passes the token to HolderRewards and JobsVault (their one-time setToken, callable only by the vault).\r\n   - distribute(): anyone, at most once every 7 days, only after bindLaunch. First calls factory.claimFees(launchNumber) inside try/catch (a failed claim never blocks distributing what the vault already holds). Then splits the vault's whole IMD balance: 40% to HolderRewards, 25.72% to JobsVault, 17.14% to the dev wallet, 17.14% to the marketing wallet (rounding dust stays for next week). The vault's whole balance of the launched token is burned (sent to 0x000000000000000000000000000000000000dEaD). Native ETH: the pool is TEST/IMD so the factory should never pay ETH, but the vault accepts it (receive()) so a native payout can never make claimFees revert; any native ETH balance is sent 100% to the marketing wallet in distribute(). No conversions.\r\n   - No other way for IMD or tokens to leave.\r\n   SIMDVault deploys HolderRewards and JobsVault in its own constructor and exposes them as immutable public getters.\r\n2. HolderRewards: receives IMD each week and pays it to holders of the launched token pro rata to a weekly snapshot, so it cannot be gamed by buying just before a claim. On each IMD deposit from the vault it opens a new epoch and asks the swarm, through Intake.request(\"oracle.request@oracle-1\", ...), paid from its own IMD, for a Merkle root of every holder's balance at the deposit block, excluding pools, the PoolManager, the factory and these three contracts; the root arrives in the oracle callback (only Intake can call it). claim(epoch, amount, proof) pays each holder once per epoch, and an epoch can never pay out more than its pot: each epoch stores its pot and claimed total, and a claim reverts if claimed + amount would exceed the pot. After 28 days anyone can roll an epoch over: exactly pot - claimed moves into the current epoch and the old epoch closes for claims.\r\n3. JobsVault: receives IMD each week and pays Identity.md swarm jobs for holders. Any wallet holding at least 1,000,000 of the launched token can call requestJob(bytes body) at most once per 24 hours; the vault pays Intake.request(\"job.open@job-1\", body, no callback, IMD, priceOf) from its own IMD. No withdraw.\r\n\r\nTests: fork tests against Robinhood Chain with the real Intake and factory addresses; launch and bindLaunch only by the dev wallet and only once; bindLaunch rejects a launch whose requester is not the vault; the 40 / 25.72 / 17.14 / 17.14 split and the token burn; the 7-day limit; JobsVault holding and daily limits; only Intake can deliver oracle answers; a native-ETH payout during distribute() goes to the marketing wallet without blocking the IMD split or the burn; an over-sized Merkle claim and a double claim revert; rollover moves exactly pot - claimed.\r\n","parentJobId":null,"planHash":"4aee5b59657851c205a7c107d140ed977c7eddb9f9f0e33c6d300753a9db5516","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"796405ca-1aef-4b22-945a-61cadefb4cb4","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2355-simd-vault-robinhood-chain"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52207","feedbackHash":"70946d0bbcba0de21ae528c867d83841a005a30cc1961139b6c769d7209e35e6","nodeKey":"audit_economics","submissionHash":"9259e52e222c21433affb416cfcdf87f9016f1b35934c54d0d4c6403de73d4ac","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51130","feedbackHash":"a4671b178956c9adb8862fcdfbed1e7d9b19ab25041ea7015398abeffbb05ffa","nodeKey":"audit_flow","submissionHash":"b8d43fb0e363ef45e7979aceb7dbe24eb31a7071f95fab20bb7bb37169583fe0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51250","feedbackHash":"97527560e8bdadb96d7484f0ed6f065dde0e249064dd47e27e80c52e2a3fda75","nodeKey":"audit_judge","submissionHash":"fd8bbf1d1a0ad0195ef5a175c489cbcbbd1481f2132cec74cfb511938698754d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52609","feedbackHash":"f0f4fc4c74d96069cea01ee84d869f65131c39ecc2fe10cacaeac800455dc7d1","nodeKey":"audit_math","submissionHash":"1a74b63773d442360d7076da0ea1c3c86cf2669c894528d6f13c18d9eec43a8b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51566","feedbackHash":"1e2d90b5c044c066c027c194074ee735acc96386642c2331ed748e9a9d2db9a6","nodeKey":"audit_permissions","submissionHash":"a9956308d7ef54cbd4fc1ec5d084d6e185119d35c9e8edf1b6f3b9eefc424736","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51251","feedbackHash":"b4285a7a4eafd0f697b8e2287597075d795beba251bcade76aad16cd4549efd8","nodeKey":"build_contract_project","submissionHash":"c5e5a95283a5e00a1320e0140be1beea3a89d460b3f23f23f018bba93c2b4d19","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52114","feedbackHash":"f3c1133113e4eeeac702db9e3e78b27a25c94cea777b5c4de8fdbadd83c2ff0a","nodeKey":"build_contract_project","submissionHash":"236906d034f736ba79732dd39e72f3431b7fc495ae3658ca691f7d2382c6e615","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52642","feedbackHash":"7c6a4f66c62d1a7d2d2decd1a63b201b966c39d5eb674b3064658ea78c31ee98","nodeKey":"build_contract_project","submissionHash":"a60ca1db144ba1fa0a8bceb926e9ab23d09ed780fcce72a39f645fa922cca065","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52292","feedbackHash":"e2cd348734b1a8aab891e0d2f8b85539c116ad84fbfa4bc78c3e49e057c995f9","nodeKey":"manifest","submissionHash":"283c30d79d3985329be180c1715a6ca9cd227cc79f7eea27ead3cf21125a469a","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51225","feedbackHash":"d8fb087aef09bc59cfd24ac800b3ad97eb5c28154100557a376a70fbcc3cc939","nodeKey":"write_foundry_tests","submissionHash":"b968c624c6fbdd9a553a3b9b94480af8f5e0ac6978f2600cf07e4b1be6b27be3","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51140","feedbackHash":"e6915affeb49ec3260a386f9828cfdb6753185d05fff5aad688e0edcf5b354a2","nodeKey":"write_foundry_tests","submissionHash":"948a45d9275599eecc133bda5de3a7e2e1c0b368756c29aa820087ec41e441cf","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52165","feedbackHash":"5de5cc146bfb652632a9f2f4efef3460d0d4a1f1d6a1b7b9f4b644b80cae6103","nodeKey":"write_foundry_tests","submissionHash":"9610774c1f9187877616924184ab12cbc7589be42b1782970f5cb730f62819d1","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"58a4727bf6b26d0d4d6085d00285d7239ac25852519e99d869506786de67811e","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8e25456c4ae69acf","findings":[{"citation":"resolved","description":"rollover() adds pot - claimed of an old epoch to epochs[current].pot, and claim() computes every payout from the live pot (src/HolderRewards.sol:214 `payout = e.pot * balance / e.total;`). The current epoch usually already has its root and has been partly claimed when the rollover lands (each rollover happens about 28 days after the old epoch opened, roughly 3 weeks into the newer epoch). Holders who claimed before the rollover were paid pot_old * b / total. Holders who claim afterwards get (pot_old + rolled) * b / total. hasClaimed stops the earlier holders from collecting the difference. So the 'pro rata to the snapshot' guarantee breaks for every epoch that receives a rollover after its first claim, and the PotExceeded cap does not catch it because payouts only ever rise. Anyone can call rollover(), so a large holder can wait for an old epoch to pass 28 days, roll it over, then claim, taking a pro-rata cut of the rolled amount that earlier claimers in the same epoch never see. The leftover (rolled * share of early claimers) is not destroyed; it stays as pot - claimed and moves on at a later rollover. Value still shifts from holders who claim promptly to holders who claim late. Seam: precision x invariant (a pot that changes over time feeds a ratio that is assumed fixed). Fix: keep the pot each holder is paid from fixed for the epoch's claim period. One way is to roll the remainder into the next epoch opened rather than into one already being claimed, for example by adding a pending carry to the pot in _openEpoch. Another is to pay a top-up so a holder who already claimed can collect pot*balance/total - alreadyPaid. Either way the spec's rule that exactly pot - claimed moves forward still holds.","line":236,"path":"src/HolderRewards.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Fixed} from \"src/Fixed.sol\";\nimport {HolderRewards} from \"src/HolderRewards.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ScratchERC20 {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a; balanceOf[to] += a; return true;\n    }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\ncontract ScratchIntake {\n    uint256 public nonce;\n    function priceOf(bytes32, address) external pure returns (uint256) { return 1 ether; }\n    function request(bytes32, bytes calldata, IIntake.Callback calldata, address asset, uint256 amount)\n        external payable returns (bytes32 id)\n    {\n        ScratchERC20(asset).transferFrom(msg.sender, address(this), amount);\n        id = keccak256(abi.encode(++nonce));\n    }\n}\n\ncontract ScratchSigner {\n    function isValidSignature(bytes32, bytes calldata) external pure returns (bytes4) { return 0x1626ba7e; }\n}\n\n/// A rollover into an epoch that is already being claimed changes the per-token rate mid-epoch:\n/// holders who claimed before it get pot_old * b / total, those after get (pot_old + rolled) * b / total.\ncontract RolloverProRataTest is Test {\n    HolderRewards rewards;\n    ScratchERC20 imd = ScratchERC20(Fixed.IMD);\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        vm.roll(1_000);\n        vm.etch(Fixed.IMD, address(new ScratchERC20()).code);\n        vm.etch(Fixed.INTAKE, address(new ScratchIntake()).code);\n        vm.etch(Fixed.ORACLE_SIGNER, address(new ScratchSigner()).code);\n        rewards = new HolderRewards(Fixed.ORACLE_SIGNER); // this test plays the vault\n        rewards.setToken(address(0xBEEF), address(0xCAFE));\n        imd.mint(address(this), 1_000 ether);\n        imd.approve(address(rewards), type(uint256).max);\n    }\n\n    function _deliver(uint256 epoch, bytes32 requestId, bytes32 root, uint256 total) internal {\n        HolderRewards.Epoch memory e = rewards.epochInfo(epoch);\n        OracleAttestation.Attestation memory a = OracleAttestation.Attestation({\n            requestId: keccak256(abi.encode(\"req\", epoch)), chainId: block.chainid, questionHash: e.questionHash,\n            answerType: OracleAttestation.ANSWER_BYTES32, answer: abi.encode(root), figure: total,\n            fromBlock: e.snapshotBlock, toBlock: e.snapshotBlock, blockHash: bytes32(0), panelJobId: bytes32(0),\n            panelSize: 5, quorum: 3, agreed: 5, issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 days)\n        });\n        vm.prank(Fixed.INTAKE);\n        rewards.onOracleResult(requestId, a, \"\");\n    }\n\n    function test_holdersWithEqualBalancesGetEqualPayoutsAcrossARollover() public {\n        bytes32 la = keccak256(bytes.concat(keccak256(abi.encode(alice, uint256(50)))));\n        bytes32 lb = keccak256(bytes.concat(keccak256(abi.encode(bob, uint256(50)))));\n        bytes32 root = la < lb ? keccak256(abi.encodePacked(la, lb)) : keccak256(abi.encodePacked(lb, la));\n        bytes32[] memory pa = new bytes32[](1); pa[0] = lb;\n        bytes32[] memory pb = new bytes32[](1); pb[0] = la;\n\n        // Week 0: epoch 1, pot 100 IMD; nobody claims it.\n        rewards.deposit(101 ether);\n        // Day 21: epoch 2, pot 100 IMD; alice and bob each hold half of the snapshot.\n        vm.warp(block.timestamp + 21 days);\n        vm.roll(block.number + 100);\n        rewards.deposit(101 ether);\n        _deliver(2, keccak256(abi.encode(uint256(2))), root, 100);\n        assertEq(rewards.epochInfo(2).pot, 100 ether);\n\n        vm.prank(alice);\n        uint256 alicePaid = rewards.claim(2, 50, pa); // 50 IMD\n\n        // Day 28: anyone rolls epoch 1's unclaimed 100 IMD into epoch 2.\n        vm.warp(block.timestamp + 7 days);\n        rewards.rollover(1);\n        assertEq(rewards.epochInfo(2).pot, 200 ether);\n\n        vm.prank(bob);\n        uint256 bobPaid = rewards.claim(2, 50, pb); // 100 IMD\n\n        // Same snapshot balance, same epoch: the pro-rata share should be equal.\n        assertEq(alicePaid, bobPaid, \"equal snapshot balances paid unequally\");\n    }\n}","reproduction":"HolderRewards with price 1 IMD. t=0: deposit(101e18) opens epoch 1, pot 100e18, unclaimed. t=21d: deposit(101e18) opens epoch 2, pot 100e18. The root for epoch 2 has leaves (alice,50) and (bob,50), figure 100. Alice claims epoch 2 and gets 100e18*50/100 = 50e18. t=28d: anyone calls rollover(1), so epoch 2's pot becomes 200e18. Bob claims epoch 2 and gets 200e18*50/100 = 100e18. Expected: equal snapshot balances in the same epoch get equal shares (75e18 each, or both get 50e18 with the 100e18 carried forward). Actual: alice 50e18, bob 100e18, and the remaining 50e18 waits for a later rollover. The attached test asserts alicePaid == bobPaid and fails with 50000000000000000000 != 100000000000000000000.","severity":"medium","snippet":"        epochs[current].pot += amount;","title":"Rollover into an epoch that is already being claimed changes its per-token rate mid-epoch: holders who claimed earlier lose their share of the rolled-in IMD"}],"hash":"1a74b63773d442360d7076da0ea1c3c86cf2669c894528d6f13c18d9eec43a8b","nodeId":"e6de3bab-83bd-4272-ae20-64561da0938b","outcome":"completed","summary":"I found one defect in the math area, a medium one. It's written to `.imd-findings.json` with a coverage row for all 13 entry points plus one invariant row. No repository files were changed; the scratch test has been deleted.\n\n**Finding 1 (medium): a rollover changes the payout rate partway through an epoch** (`src/HolderRewards.sol:236`, together with `:214`)\n\n- **Cause:** `rollover()` adds `pot - claimed` to the current epoch's pot. `claim()` pays `e.pot * balance / e.total` from that pot as it stands at claim time. A rollover arrives about 28 days after the old epoch opened, so the current epoch is usually already being claimed.\n- **Effect:** holders who claimed before the rollover are paid at the old rate, and the once-per-epoch check stops them collecting the difference. Holders who claim afterwards get a share of the rolled-in IMD as well. Anyone can call `rollover`, so a holder can roll an old epoch over and then claim. The `PotExceeded` cap doesn't catch this because payouts only go up. No IMD is lost; the shortfall rolls forward later. It is still taken from holders who claimed promptly.\n- **Reproduction:**\n  1. Epoch 1 opens with a pot of 100 IMD and nobody claims it.\n  2. On day 21, epoch 2 opens with a pot of 100 IMD. Alice and Bob each hold 50 of 100 snapshot units.\n  3. Alice claims and gets 50 IMD.\n  4. On day 28, `rollover(1)` raises epoch 2's pot to 200 IMD.\n  5. Bob claims and gets 100 IMD. 50 IMD stays behind until a later rollover.\n- **Proof:** an attached self-contained Foundry test fails on the current code with `50e18 != 100e18`.\n- **Suggested fix:** either add the rollover to the next epoch to open instead of one already being claimed, or let a holder who already claimed collect `pot*balance/total - alreadyPaid`. Either way, exactly `pot - claimed` still moves forward, as the brief requires.\n\n**What holds in the math area**\n- **Weekly split:** the percentages are 4000/2572/1714/1714 out of 10000. Each share rounds down, and at most 3 wei of dust carries to the next week.\n- **No overflow:** none of the `a*b/c` steps can overflow at IMD and token scale.\n- **Rewards bookkeeping:**\n  - The pot is `free - price`, where `free` is the balance minus what open epochs still owe.\n  - The amount owed always equals the sum of open `pot - claimed` across deposit, claim and rollover.\n  - Rollover moves exactly `pot - claimed`.\n- **Time limits:** each opens exactly on its boundary (7 days, 28 days, 24 hours), and an attestation is still valid at its expiry second.\n- **Jobs threshold:** the 1,000,000-token minimum is compared in the same smallest unit as the snapshot balances.\n\nThe existing suite passes: 86 tests passed and 9 skipped (the fork tests, which need a fork RPC URL and so skip here).\n\n**Leads I left out**\n- **Decimals overflow:** `JobsVault.setToken` overflows only if the token reports 72 or more decimals. That would block `bindLaunch` for good, but a factory-launched token doesn't have that many decimals.\n- **Last epoch's remainder:** what the last epoch still owes can't roll over if no newer epoch ever opens.\n- **Claim window:** the 28-day clock starts when the epoch opens, not when its root arrives, so a late root shortens the time holders have to claim.\n\nStatic-analysis leads checked and rejected:\n- **Unchecked ETH send:** the destination is a fixed constant wallet.\n- **Ignored `positionOf` return values:** the unused tick values don't matter.\n- **Reentrancy:** these are calls to trusted fixed contracts.\n\nI only went into areas outside math (access control, oracle trust, gaming the snapshot) as far as needed to trace the math paths.","treeHash":null,"usage":{"cachedInputTokens":966911,"inputTokens":28,"model":"claude-opus-5-5","outputTokens":22399,"runtime":"claude","turns":18,"wallClockMs":272055}},{"artifacts":[],"attempt":1,"bundleHash":"be2624d68232e13ae7e7ff7a3442c4795d923ab10dadeca341c7b327375ae041","device":"918f8261a6fd589c","findings":[],"hash":"236906d034f736ba79732dd39e72f3431b7fc495ae3658ca691f7d2382c6e615","nodeId":"f701b8e8-4f56-4b4f-8937-9e58aa83d987","outcome":"completed","summary":"The responses file is excluded from git by the harness's own rule, so it is read from the working tree as intended. Everything is in place.\n\n**Outcome:** the reopened defect is fixed and its proof passes; two advisory findings are fixed; one is disputed as the brief's specified behaviour. `forge build`, `forge test` (70 passed, 4 fork tests skipped offline) with the verifier's fuzz seed, and `forge fmt --check` all pass. The fork suite also ran live against Robinhood Chain and passed 4/4.\n\n**What changed**\n\n- **JobsVault (medium, fixed):** eligibility now comes from the latest oracle snapshot instead of the caller's live balance alone. A holder calls `proveHolding(epoch, balance, proof)` with the same Merkle leaf it uses in HolderRewards, for the newest epoch that has a root. `requestJob(bytes)` keeps its signature and requires a proof on that snapshot, a live balance of at least 1,000,000 tokens, and the 24-hour per-wallet cooldown. One holding sits at one address per snapshot, so it opens at most one job per day however many wallets it passes through, and a flash-borrowed balance has no leaf. `proveAndRequestJob` does both steps in one call.\n- **HolderRewards:** tracks `latestRootedEpoch` in the oracle callback (about 115k of the 200k stipend used) and exposes `rootOf`. The oracle ask is now inside a `try`, so a reverting Intake holds the 40% for the next epoch instead of freezing the whole distribution.\n- **SIMDVault:** `bindLaunch` rejects IMD as the launched token, and passes the HolderRewards address to the JobsVault constructor. SIMDVault still has zero constructor arguments.\n- **Tests:** the recycled-holding test was replaced with one asserting exactly one job for a holding moved through ten wallets, plus tests for flash balances, sub-minimum snapshot balances, foreign proofs, root replacement, no jobs before the first root, the frozen-distribute scenario from the finding, a reverting `priceOf`, and the IMD bind. The fork test proves a leaf before opening a job against the real Intake.\n- **README:** documents the snapshot rule, its residual (a holding moved between two weekly snapshots can open two jobs around the week the root changes, never more), the isolated oracle ask, the IMD rejection, and the new sizes.\n\n**Disputed:** the rollover fairness note. Rollover moving exactly pot minus claimed into the current epoch is what the brief specifies, nothing is overpaid or lost, and the README already states that later claimers in a rolled-into epoch get a proportionally larger share.","treeHash":"b89a6041f704ca796d769014fe6016c8b905c1ca","usage":{"cachedInputTokens":1910972,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":45280,"runtime":"claude","turns":46,"wallClockMs":580844}},{"artifacts":[],"attempt":1,"bundleHash":"3d5638d77730abe9c78a2b48b0b87f71ca0ff90877d70167de4f05f4d14fd929","device":"ddcecd7483f623f7","findings":[],"hash":"283c30d79d3985329be180c1715a6ca9cd227cc79f7eea27ead3cf21125a469a","nodeId":"a83cc836-8345-42cd-9677-4938e2f7a141","outcome":"completed","summary":"Those untracked entries are sandbox device masks owned by nobody, not files I created, so they are outside my work and I left them alone.\n\n**Done.** I wrote `launch.json` as the only changed file. The accepted source has one top-level application contract, so the manifest lists that one entry.\n\n- **SIMDVault** with an empty constructor argument list. Its constructor creates HolderRewards and JobsVault itself and calls no outside contract, so it deploys on an empty chain.\n- **HolderRewards and JobsVault are not listed.** The vault deploys them in its constructor and exposes them through its immutable getters. Separate factory instances would not be linked to the vault, so naming them would be wrong.\n- **No owner argument** appears anywhere. None of the three contracts has an owner, admin, upgrade or pause. Every address and rate is a constant, matching the brief and the README.\n- **Notes** describe the fixed addresses, the three contracts' behaviour and the post-deployment steps, at under 2,800 characters.\n\nChecks I ran:\n\n- The manifest parses as JSON with exactly the keys kind, contracts and notes, and the contract name is 9 ASCII characters.\n- `forge build` succeeds.\n- `forge test` with the verifier's fuzz seed passes 86 tests with 9 skipped. The skipped ones are the fork tests, which skip cleanly because no RPC URL is configured here.\n- `git status` shows launch.json as the only new file of mine.\n\nFork runs against live Robinhood Chain state are still owed by whoever has network access, as the test suite documents.","treeHash":"59c594941f6881a99d27afb8ca5c4717c9937a63","usage":{"cachedInputTokens":222748,"inputTokens":162,"model":"claude-fable-5-1","outputTokens":3406,"runtime":"claude","turns":6,"wallClockMs":83573}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"27168b1ff68f84d2","findings":[],"hash":"408a99607d4c3aefdc8c8ecbb48fce939cc5426ded17f56d286d8f9fb52c3865","nodeId":"a83cc836-8345-42cd-9677-4938e2f7a141","outcome":"failed","summary":"needs input: A test-suite repair outside this task's write budget (launch.json only). The manifest is complete and correct (SIMDVault, zero constructor arguments) and `forge build` passes, but `forge test` fails on this machine's forge 1.8.5: it treats chain id 4663 as an Arbitrum chain and answers `ArbSys(address(100)).arbBlockNumber()` with `block.number` itself, ignoring the MockArbSys the tests etch at address(100) (verified: the same mock returns the stored value on chain ids 1 and 31337 and `block.number` on 4663 and 42161). The suite sets `vm.chainId(4663)` and `arbSys.set(1_000_000)` but never `vm.roll`, so block.number stays 1 and HolderRewards._openEpoch's `head() - WINDOW_DEPTH` (8) underflows in every distribute() path, which is the 26 arithmetic-underflow failures and the broken invariant. The fix is in test files only: add `vm.roll(HEAD)` in test/Base.t.sol setUp and replace/accompany each `arbSys.set(n)` in test/EdgeCases.t.sol and test/invariant/SystemInvariant.t.sol with `vm.roll(n","treeHash":null,"usage":{"cachedInputTokens":1357307,"inputTokens":836,"model":"claude-fable-5-1","outputTokens":18383,"runtime":"claude","turns":31,"wallClockMs":417347}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f009de0ce52c7b3a","findings":[{"citation":"resolved","description":"Economic Security / Flow Gap (execution x periphery x first principles). `launched` is set the moment the Intake accepts the payment, but the Intake only takes the price: whether a launch is actually created is decided off chain afterwards (the intake-client reference: 'whatever the plane refuses is refused with status 1 and the price is spent'; the JobsVault comment in this tree says the same). The vault asks with plain `request` and no failure hook, so a refusal (status 1) or a failed launch (status 2) is never reported to the contract and can never be distinguished from success. After one refused body: `launch` reverts `AlreadyLaunched` forever, `bindLaunch` reverts `NotTheRequester` for every launch number because no launch names the vault as requester, `distribute` reverts `NotBound`, and there is no other way for IMD to leave. Every IMD the dev wallet funded the vault with (the brief: 'the vault must be funded with IMD before the dev wallet calls launch') is stranded, and the whole product has to be redeployed and refunded. The brief asks for 'only once', which is satisfied for a successful launch; a refused request is not a launch. The Intake v2 at the fixed address offers `requestWithFailure(action, body, callback, onFailure, asset, amount)`: asking with it and an `onLaunchFailure` (only callable by the Intake, for `launchRequestId`) that clears `launched` keeps 'only once' for real launches and removes the brick. Tradeoff: this adds one Intake-only entry point; it does not add an owner or admin.","line":85,"path":"src/SIMDVault.sol","reproduction":"State: fresh SIMDVault, funded with 100 IMD, Intake priceOf(launch.open@launch-1, IMD) = 0.5 IMD. (1) dev calls launch(body) with a body the plane later refuses (any body the launch schema rejects). Tx succeeds, 0.5 IMD goes to the Intake, launched == true. (2) Off chain: status 1, no launch is created, no factory position has requester == vault. (3) dev calls launch(fixedBody): reverts AlreadyLaunched. (4) dev calls bindLaunch(n, token) for any n: reverts NotTheRequester(n, <not the vault>). (5) anyone calls distribute(): reverts NotBound. Expected: the vault can ask again after a refusal, or at least the IMD can reach the split. Actual: 99.5 IMD sit in the vault with no function that can move them. Reproduced in test/scratch/Econ.t.sol test_refusedLaunchBricksVault (mock Intake at the fixed address; log 'IMD stuck in vault: 99500000000000000000').","severity":"medium","snippet":"        if (launched) revert AlreadyLaunched();\n        if (body.length == 0) revert EmptyBody();\n        launched = true;","title":"launch() is spent on the first call even when the plane refuses the body, which bricks the vault and strands its IMD for good"},{"citation":"resolved","description":"Economic Security ('break dependencies: construct a failure that permanently blocks claims'). HolderRewards and JobsVault pay out only through Intake.request on a fixed action id and accept roots only from a fixed signer address; the oracle-consumer reference for this network states that 'the intake, the action id, the asset and price, the signer' must never be constants because 'a new action version (oracle-2) or a moved price must not need a redeploy'. The brief's 'no owner, no admin' is why they are constants here, so this is a design tradeoff the requester must decide, but the economic consequence is not stated anywhere in the code or the manifest: when `priceOf(ORACLE_REQUEST, IMD)` becomes 0 (action retired) or the Intake stops accepting the body, `deposit` holds every later deposit (line 135) and no new epoch opens; the last opened epoch is `currentEpoch`, so `rollover` on it reverts `NoNewerEpoch` (line 231), and with no root its `claim` reverts `NoRoot`. 40% of all fees routed there after the retirement, plus the last epoch's pot, can never leave. If the signer at 0x5598... is rotated instead, every answer fails `BadSignature`, so each epoch is rootless and the remainder just hops from epoch to epoch every 28 days without ever being claimable. JobsVault: `requestJob` reverts `ActionNotSold` and the 25.72% share accumulates with no withdraw. SIMDVault keeps routing 65.72% of every week's fees into the two dead contracts. A fix that keeps 'no admin' is a dead-man's switch: e.g. if HolderRewards has held deposits (no epoch opened) for N consecutive weeks, `deposit` forwards the free IMD back to the vault's dev/marketing split or holds it in the vault instead of the child; same for JobsVault after N weeks without a job. Alternatively accept and document the risk in the manifest notes.","line":135,"path":"src/HolderRewards.sol","reproduction":"State: launched and bound, vault funded. (1) distribute() opens epoch 1 (pot 39.3 IMD). (2) Intake price for oracle.request@oracle-1 set to 0 (retired). (3) Three weekly distribute() calls with 100 IMD of fees each: each emits DepositHeld, currentEpoch stays 1, HolderRewards balance 159.3 IMD, reserved 39.3 IMD. (4) warp 100 days; rollover(1) reverts NoNewerEpoch(1); claim(1, ...) reverts NoRoot(1). (5) job.open@job-1 price set to 0: requestJob reverts ActionNotSold; JobsVault holds 102.75 IMD with no function that can move it. Expected: IMD routed to holders and jobs stays reachable, or stops being routed once the dependency is gone. Actual: 262 IMD stranded after 3 weeks and growing 65.72% of fees per week. Reproduced in test/scratch/Econ.t.sol test_retiredOracleActionStrandsHolderImd.","severity":"medium","snippet":"        if (price == 0 || free <= price || !_openEpoch(free - price, price)) emit DepositHeld(amount, free);","title":"Every protocol handle is a constant with no exit path: once oracle.request@oracle-1 (or job.open@job-1, or the signer key) is retired, all IMD already in HolderRewards and JobsVault is stranded foreve"},{"citation":"resolved","description":"Flow Gap, seam execution x first principles: the brief's purpose is to pay 'holders of the launched token pro rata to a weekly snapshot, so it cannot be gamed by buying just before a claim'. The literal claim-side guarantee holds (the root is fixed before any claim). But the snapshot block is derived from the block in which `SIMDVault.distribute()` runs, and distribute is callable by anyone at any time after `lastDistributionAt + 7 days` (SIMDVault.sol:118-122). WINDOW_DEPTH = 8 exists for finality, not for anti-gaming: on an Arbitrum Orbit chain 8 blocks is on the order of 2 to 8 seconds. An actor can therefore buy TEST at block b, call distribute at block b+8 (or wait for the week's boundary and do both), and sell at b+9, and be counted at full weight in the week's pot, diluting every real holder. Economics: pot = 40% of the week's IMD creator fees (W). Buying a fraction f of the included supply (market value M of included holders) costs about 2 x poolFee x f x M plus price impact (the mock pool fee is 1%, so about 2% of the position) and earns 0.4 x W x f/(1+f). Break-even is W of roughly 5% of M per week; in a launch week of a small-cap token with turnover above a few hundred percent that is met. Even when not profitable for an outsider, an existing holder can top up for the snapshot and sell after. Fix options that keep the design: snapshot at a block the caller cannot pick (e.g. the block of the previous distribution, fixed when the previous epoch opened; or `lastDistributionAt`-anchored block recorded at the prior distribute), or a time-weighted balance question over the whole week instead of one block.","line":141,"path":"src/HolderRewards.sol","reproduction":"State: launched, bound, lastDistributionAt + 7 days passed. (1) At L2 block 4_999_992 alice swaps IMD for TEST (holding 0 before). (2) At block 5_000_000 alice calls vault.distribute(); HolderRewards opens epoch 1 with snapshotBlock == 4_999_992 (asserted in test/scratch/Econ.t.sol test_snapshotIsEightBlocksBeforeCallerChosenDistribute: epochInfo(1).snapshotBlock == 5_000_000 - 8). (3) At block 5_000_001 alice swaps TEST back to IMD. (4) The oracle's root for block 4_999_992 includes alice at her full bought balance; alice later claims pot * balance / total. Expected: a balance held for seconds around a caller-chosen block does not earn a week of fees. Actual: it earns the same as a week-long holding of the same size; every genuine holder's share shrinks by alice's weight.","severity":"medium","snippet":"        uint64 snapshotBlock = uint64(head() - WINDOW_DEPTH);","title":"The holder snapshot is taken 8 blocks before a permissionless distribute(), so the snapshot moment is chosen by whoever calls it and a 2-second holding earns a full week's share"},{"citation":"resolved","description":"Invariant ('break commutativity': A.action then B.action vs B then A yields different state). `claim` computes `payout = e.pot * balance / e.total` (line 214) from the pot at claim time and marks `hasClaimed`, while `rollover` adds `pot - claimed` of an older epoch to `epochs[currentEpoch].pot` whenever `openedAt + 28 days` has passed. With weekly distributions, the current epoch at day 28 is the one opened at day 21, whose root arrived and whose claims have been running for up to a week, so this ordering is the normal case, not an edge. A holder who claimed before the rollover is paid pot_old * b / total; one who claims after is paid (pot_old + R) * b / total. The first holder cannot claim again (`AlreadyClaimed`), so their share R * b / total stays in the epoch and, 28 days later, rolls into whichever epoch is current then, whose holder set may differ. Anyone can call rollover, so a large holder simply waits for others to claim, rolls, then claims. The cap `claimed + payout <= pot` keeps the epoch solvent, so no overpayment occurs; the defect is pro-rata broken by ordering and value drifting to later epochs. Fix: credit rollovers to the next epoch that opens (a `pendingRollover` added to the pot in `_openEpoch` before any claim is possible) instead of the current one, or snapshot `pot` per epoch at root arrival and send rollovers only to epochs without a root.","line":236,"path":"src/HolderRewards.sol","reproduction":"State: epoch 1 opened at day 0 with pot 39.3 IMD and no root (or no claims). Epoch 2 opened at day 21 with pot 39.5 IMD, root with leaves alice=50, bob=50, total 100. (1) Day 21: alice.claim(2, 50, proof) -> 19.75 IMD. (2) Day 28: anyone calls rollover(1) -> returns 39.3; epochInfo(2).pot == 78.8. (3) bob.claim(2, 50, proof) -> 39.4 IMD. (4) alice.claim(2, ...) reverts AlreadyClaimed; epochInfo(2).pot - claimed == 19.65 IMD left behind, to be rolled again at day 49. Expected: two equal holders of the same epoch receive the same payout and the rolled remainder is split pro rata among that epoch's holders. Actual: bob 39.4 vs alice 19.75 for identical leaves. Reproduced in test/scratch/Econ.t.sol test_rolloverOrderChangesPayoutForEqualHoldings (asserts bobPaid == alicePaid + rolled / 2). The existing HolderRewards tests never claim from the receiving epoch before a rollover into it, so this ordering is untested.","severity":"medium","snippet":"        epochs[current].pot += amount;","title":"rollover() grows the current epoch's pot after holders may already have claimed from it, so equal holdings are paid unequally and the early claimers' share of the rollover is deferred to a later epoch"},{"citation":"resolved","description":"Economic Security ('push fee formulas to zero'). The only threshold is `free > price`. Whenever the week's holder share is just above the oracle price, 0.5 IMD is paid to the Intake to distribute a pot that may be a few wei, and the dust is unclaimable anyway (`NothingToClaim` for most leaves) and rolls over 28 days later. Because distribute() is permissionless and anyone may call it the moment the 7 days pass, the caller, not the holders, decides whether a thin week is cut now at full oracle cost or allowed to accumulate. Over a long low-volume period this converts the holders' 40% almost entirely into oracle fees. Fix: hold the deposit unless `free >= price * K` for a small multiple K (e.g. 4), or unless `free - price` exceeds a fixed minimum pot; held IMD already carries into the next epoch, so nothing is lost.","line":135,"path":"src/HolderRewards.sol","reproduction":"State: bound; vault IMD balance 1.26 IMD; oracle price 0.5 IMD. distribute(): toHolders = 0.504 IMD; HolderRewards free = 0.504 > 0.5 -> _openEpoch(0.004 IMD, 0.5 IMD): 0.5 IMD paid to the Intake, epochInfo(1).pot == 0.004 IMD. Expected: a pot smaller than the fee it costs to open is held for a bigger week. Actual: 99.2% of the holders' share is spent on the oracle. Reproduced in test/scratch/Econ.t.sol test_dustPotSpendsFullOraclePrice (logs pot 4000000000000000, paid to oracle 500000000000000000).","severity":"low","snippet":"        if (price == 0 || free <= price || !_openEpoch(free - price, price)) emit DepositHeld(amount, free);","title":"No minimum pot: an epoch is opened and the full oracle price spent even when the pot is dust"},{"citation":"resolved","description":"Flow Gap, seam execution x periphery. The timer is advanced before and regardless of the outcome of the low-level `claimFees` call. If the factory reverts at that moment (pool manager state, factory pause, out-of-gas on a large position) the vault distributes whatever it already holds (often nothing) and the fees stay in the factory for at least 7 more days. Anyone may call distribute, so a caller who times the call to a failing moment delays the week; the delay is bounded (the fees are not lost) and the factory is trusted, hence low. A safer rule within the brief: only advance `lastDistributionAt` when `claimed` is true or when `total > 0`, so a no-op distribution does not spend the slot.","line":122,"path":"src/SIMDVault.sol","reproduction":"State: bound; vault holds 0 IMD; factory.claimFees reverts this block while 100 IMD of fees are claimable. (1) anyone calls distribute(): FeesClaimed(n, false), Distributed(0, 0, 0, 0, 0, 0), lastDistributionAt = now. (2) Next block the factory works again; distribute() reverts TooEarly(now + 7 days). Expected: a distribution that moved nothing does not block the next one. Actual: 100 IMD of fees wait 7 days. Reproduced in test/scratch/Econ.t.sol test_failedClaimStillConsumesTheWeek.","severity":"low","snippet":"        lastDistributionAt = block.timestamp;","title":"A distribute() whose claimFees fails still consumes the 7-day slot, so a transient factory failure delays that week's fees by a full period"},{"citation":"resolved","description":"Economic Security / Flow Gap. The ask uses `request` with no `onFailure`. A refused body (status 1), a panel that does not settle (status 2), or a delivery after `expiresAt` (`AttestationExpired` in `_verifyAttestation`, which reverts the callback) leaves the epoch rootless with no on-chain signal. The pot is then unreachable until `rollover` becomes possible at `openedAt + 28 days`, and only if a newer epoch exists. With `requestWithFailure` and an Intake-only `onOracleFailure(requestId, ...)` the contract could mark the epoch failed and let its pot fold into the next epoch at the next deposit (or re-ask once from the held IMD) instead of waiting four weeks. The oracle-consumer reference describes exactly this pattern for version 2 intakes ('the contract knows the price is spent and can ask again').","line":147,"path":"src/HolderRewards.sol","reproduction":"State: bound; distribute() opens epoch 1 with pot 39.3 IMD at day 0. The plane refuses the ask (or delivers at day 2: onOracleResult reverts AttestationExpired). (1) Any claim(1, ...) reverts NoRoot(1) for 28 days. (2) rollover(1) reverts RolloverTooEarly until day 28 and NoNewerEpoch unless a later distribute opened epoch 2. Expected: a failed ask is recognised and the pot moves on at the next weekly epoch (day 7). Actual: 39.3 IMD are frozen until day 28 at the earliest.","severity":"low","snippet":"        try INTAKE.request(\n            ORACLE_ACTION, body, IIntake.Callback(address(this), this.onOracleResult.selector), address(IMD), price\n        ) returns (","title":"An oracle ask that is refused, times out or is answered after its 1-day validity locks the epoch's pot for 28 days instead of being retried; the Intake's failure hook is not used"},{"citation":"resolved","description":"Trust assumption, not a bypass: only the dev wallet can bind. The brief says 'the pool is TEST/IMD so the factory should never pay ETH'. If the launch body ends up with a native-ETH pool (currency0 == address(0)), the check passes, every claimFees pays ETH plus TEST, 100% of the ETH goes to the marketing wallet and nothing ever reaches HolderRewards or JobsVault. Adding `if (key.currency0 != address(IMD) && key.currency1 != address(IMD)) revert` costs nothing and makes the brief's economics a contract guarantee rather than a dev-wallet promise.","line":107,"path":"src/SIMDVault.sol","reproduction":"State: factory.positionOf(n) returns key {currency0: 0x0 (ETH), currency1: TEST, ...}, requester == vault. dev calls bindLaunch(n, TEST): succeeds. Every distribute(): IMD split is 0, ETH -> marketing, TEST burned. Expected (brief): TEST/IMD pool, 40% of fees to holders. Actual: holders and jobs get nothing; bound for good.","severity":"info","snippet":"        if (key.currency0 != token_ && key.currency1 != token_) revert TokenNotInPool(launchNumber_, token_);","title":"bindLaunch checks that the token is in the pool but not that the other currency is IMD, so a TEST/ETH launch would route all IMD-side value past the holders"}],"hash":"9259e52e222c21433affb416cfcdf87f9016f1b35934c54d0d4c6403de73d4ac","nodeId":"8ec34ecb-c88b-4600-a301-ecbadd951be2","outcome":"completed","summary":"The findings file is valid, every snippet matches the tree, and the coverage record answers all 13 entry points. The scratch tests are in the gitignored test/scratch directory and nothing else in the tree changed.\n\n## Review result\n\nEight findings are written to `.imd-findings.json`: four medium, three low, one info. No critical or high defects were found, so no proof files were attached. All six supporting reproductions in test/scratch/Econ.t.sol pass against the current code.\n\n**Medium findings**\n\n- **Refused launch bricks the vault.** The one-shot flag in `launch` is set when the Intake takes the price, not when a launch exists. If the plane refuses the body, the dev wallet cannot retry, no launch ever names the vault as requester, and all IMD in the vault is stranded. The Intake v2 failure hook would let a refused ask be retried while keeping \"only once\" for real launches.\n- **Constant action ids and signer with no exit.** If the oracle or job action is retired or the signer rotates, held deposits never open an epoch, the last epoch can never be rolled over, and JobsVault reverts on every job. The vault keeps routing about two thirds of every week's fees into the two dead contracts. The brief's no-admin rule forces the constants, so this is a tradeoff for the requester, with a dead-man's switch suggested as a no-admin fix.\n- **Caller-chosen snapshot.** The snapshot sits 8 blocks before a permissionless `distribute`, so a buy, an 8-block wait, a distribute call and a sell earns a full week's share. The literal claim-side guarantee in the brief holds, but the economic intent does not.\n- **Rollover order dependence.** A rollover credits the current epoch after holders may already have claimed from it. Two equal leaves were paid 19.75 and 39.4 IMD in the reproduction, and the early claimer's share drifts into a later epoch. The existing suite never claims from the receiving epoch before a rollover into it.\n\n**Low and info**: no minimum pot so a dust epoch spends the full oracle price, a failed fee claim still consumes the weekly slot, an oracle failure locks a pot for 28 days instead of being retried, and `bindLaunch` does not require the pool's other side to be IMD.\n\n**What held**: the 40/25.72/17.14/17.14 split and dust handling, the burn and native ETH path, the pot cap and once-per-epoch claim, the balance-equals-reserved conservation in HolderRewards, the Intake-only oracle callback with pinned question and consumed UUID, and all JobsVault gates.\n\n**Not reached**: live Robinhood Chain behaviour. There is no network here, so the nine fork tests were skipped and the plane's acceptance of the oracle question body remains owed.","treeHash":null,"usage":{"cachedInputTokens":1136638,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":43269,"runtime":"claude","turns":24,"wallClockMs":703140}},{"artifacts":[],"attempt":1,"bundleHash":"62a1e5c9e26d4e014772dce3fc818e029047db68e70cca53c74e9d472db9423b","device":"ddc5542bddcad225","findings":[{"description":"This is new. It comes from the fix to the earlier JobsVault finding (2207eb88), which holds up: with no root ever delivered, a live holder now opens a job once HolderRewards.isDormant() (test/EdgeCases.t.sol test_theReportedNoRootReproductionNowOpensAJobAfterSixtyDays). But deposit() starts the dormancy clock (stalledSince) on every deposit, including one it holds because free < price * (MIN_POT_PRICE_MULTIPLE + 1), and in that case no oracle.request is made at all. A quiet launch whose 40% share stays under 1.5 IMD for 60 days therefore counts as 'the oracle has gone silent' while oracle.request is sold and healthy and was never paid for. Two effects follow. (1) Anyone can call HolderRewards.recoverDormant(), which sends the holders' held IMD to the dev wallet. (2) JobsVault._requestJob and canRequest accept the live balance instead of a snapshot proof, which brings back the anti-recycling defect fixed in the previous round: one 1,000,000-token holding passed from wallet to wallet opens one job per wallet in the same block. Both last until a root arrives, and no root can arrive until enough IMD has built up to open an epoch. The amounts are small: under 1.5 IMD of holder IMD per stall, and JobsVault's balance. The README describes dormancy as a dead oracle (retired action, rotated signer, refused question), which this case is not. A fix that keeps the author's design: start stalledSince only when an ask is attempted and fails, or is made and not answered (inside _openEpoch, on success or in its catch), or when the action is unsold (price == 0), not when a deposit is held only because it is below the minimum pot.","line":159,"path":"src/HolderRewards.sol","reproduction":"Mock setup from test/Base.t.sol (oracle.request sold at 0.5 IMD). _launchAndBind(). Move all but 1 IMD out of the vault. distribute(): 0.4 IMD goes to HolderRewards and is held, with no epoch and no Intake payment. Eight more times: warp 7 days, mint 0.1 IMD to the vault, distribute(); each deposit is held and no ask is made. At START + 60 days: currentEpoch == 0, the Intake received nothing for an oracle ask, and priceOf(oracle.request) == 0.5 IMD. Expected: isDormant() == false, recoverDormant() reverts NotDormant, and JobsVault still requires a snapshot proof. Actual: isDormant() == true; recoverDormant() called by anyone sends 0.72 IMD of holder rewards to the dev wallet; with JobsVault funded, alice (1,000,000 TEST live, no proof) opens a job, transfers the tokens to bob, and bob opens a second job in the same block.","severity":"low","title":"HolderRewards.isDormant() turns true when the oracle was never asked (weeks below the 3 x price threshold), sending holders' IMD to the dev wallet and waiving JobsVault's snapshot check"},{"description":"The brief says of JobsVault 'No withdraw', and of HolderRewards that it pays holders. This round adds permissionless recoverDormant() paths to both. They send IMD to Fixed.DEV_WALLET: in JobsVault after the job action has been unsold for 60 days, and in HolderRewards after 60 days of no root following a deposit, which also covers any epoch pot rolled into the carry. The conditions are narrow and documented, and the tests confirm they hold: test/EdgeCases.t.sol test_jobsRecoverDormantFailurePaths. Even so, this changes who can end up with the jobs' and holders' share, so the requester should accept it explicitly. It is not asserted as a defect. Through the low finding above, the HolderRewards path can be reached without any oracle failure.","line":191,"path":"src/JobsVault.sol","reproduction":"Read src/JobsVault.sol recoverDormant() and src/HolderRewards.sol recoverDormant() against the brief's 'JobsVault: ... No withdraw' and 'No other way for IMD or tokens to leave'. test/HolderRewards.t.sol test_aRetiredOracleActionLeavesNothingStrandedForGood shows epoch 1's whole pot plus held deposits reaching the dev wallet.","severity":"info","title":"JobsVault.recoverDormant and HolderRewards.recoverDormant add an IMD exit to the dev wallet that the brief does not have"}],"hash":"948a45d9275599eecc133bda5de3a7e2e1c0b368756c29aa820087ec41e441cf","nodeId":"6cb2e402-c7eb-4eac-bf2a-408ae9f3e314","outcome":"completed","summary":"The finding I raised last round is fixed, and I've filed one new low-severity defect that the fix opened up. The full `forge test` run passes with the verifier's fuzz seed: 100 passed, 0 failed, 9 skipped. The 9 skipped are the fork tests, which skip cleanly because this machine has no RPC. A run against live Robinhood Chain state is still owed.\n\n**Last round's finding (2207eb88).** I re-ran my original setup: four weekly distributions with the oracle never answering. Up to one second before 60 days, `requestJob` still reverts `HoldingNotProven(alice, 0, 0)`. At exactly 60 days a live 1,000,000-token holder opens a job for 0.5 IMD. The 24-hour limit and the holding minimum still apply during that fallback. Once a root arrives the proof is required again. This is `test_theReportedNoRootReproductionNowOpensAJobAfterSixtyDays` in `test/EdgeCases.t.sol`.\n\n**Tests I changed or added:**\n- **Invariant suite.** My invariant suite was failing against the author's new rules, so I updated its model in `test/invariant/SystemInvariant.t.sol`:\n  - Payouts now use the pot as it stood when the root arrived.\n  - An epoch can roll itself over, with the remainder held in a carry for the next epoch.\n  - The reserved total includes that carry.\n  - A call whose fee claim fails while the vault holds no IMD no longer uses up the week.\n\n  I also added the new exits: both `recoverDormant` functions, `noticeUnsold`, and switching the job action's price off and on. All four invariants pass at 256 runs × depth 40, and every action is reached.\n- **`test/EdgeCases.t.sol`.** Two new tests:\n  - JobsVault's recovery failure paths: still sold, an Intake that reverts, a repeated notice, and a job resetting the clock.\n  - The new week-keeping rule. Tokens are still burned and ETH still goes to marketing on such a call, and a single wei of IMD uses up the week.\n\n**Reported in `.imd-findings.json`:**\n- **Low: HolderRewards counts as \"dormant\" even though the oracle was never asked.** Every deposit starts the 60-day clock, including one that is only held because it is below the minimum pot of 3 × price. So a quiet launch whose 40% share stays under 1.5 IMD for 60 days counts as having a dead oracle, while `oracle.request` is still sold and was never paid for. Two things follow:\n  - Anyone can call `recoverDormant()` and send the holders' held IMD to the dev wallet. 0.72 IMD in my reproduction.\n  - JobsVault drops its snapshot proof, which brings back the anti-recycling problem: one holding passed to a second wallet opened two jobs in the same block.\n\n  I confirmed this with a scratch test. I did not assert the behaviour as correct. The suggested fix is to start the clock only when an ask fails or goes unanswered, or when the action is unsold.\n- **Info: new IMD exits the brief doesn't have.** Both `recoverDormant` functions send IMD to the dev wallet, while the brief says JobsVault has no withdraw. It's narrow and documented, so I flagged it for the requester to accept explicitly rather than calling it a defect.\n\nThe only tracked changes are the two test files above. The findings file sits in the repository root as the task asks.","treeHash":"3b4724811a771a7e326fa6e63a5ee64850716f39","usage":{"cachedInputTokens":2004867,"inputTokens":46,"model":"claude-opus-5-5","outputTokens":29496,"runtime":"claude","turns":36,"wallClockMs":392964}},{"artifacts":[],"attempt":1,"bundleHash":"412abac9b360fc781cddbb63cf3ac15801864a810d583374d5176f4a6dd8837b","device":"e12f98dda6acc55f","findings":[{"description":"requestJob checks the caller's live balance and the caller's own lastRequestAt. Nothing ties the job to the holding, so the same tokens moved from wallet to wallet each open a new job, all within one block. Anyone who can borrow 1,000,000 TEST for one transaction (for example from the TEST/IMD Uniswap v4 pool through PoolManager unlock/take/settle) can spend the whole JobsVault (25.72% of every week's fees) on bodies of their choosing without holding anything. This matches the brief's literal wording, so it is a gap in the brief as much as in the code; the existing test test/JobsVault.t.sol:test_movingTokensDoesNotResetTheClock asserts the hand-off as correct (carol opens a job with alice's tokens right after alice did) and should change with any fix. A fix needs a holding that cannot be recycled inside the cooldown, such as a balance snapshot (the HolderRewards root) or a minimum holding time.","line":71,"path":"src/JobsVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {Fixed} from \"src/Fixed.sol\";\nimport {SIMDVault} from \"src/SIMDVault.sol\";\nimport {JobsVault} from \"src/JobsVault.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\nimport {ILaunchFactory} from \"src/interfaces/ILaunchFactory.sol\";\n\ncontract PToken is ERC20 {\n    constructor() ERC20(\"T\", \"T\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Stand-in Intake: sells every action at 0.5 IMD and pulls exactly the amount.\ncontract PIntake {\n    uint256 public nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function request(bytes32 action, bytes calldata, IIntake.Callback calldata, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32)\n    {\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        return keccak256(abi.encode(++nonce, msg.sender, action));\n    }\n}\n\n/// @dev Stand-in factory: one position whose pool is (c0, c1) and whose requester is set by the test.\ncontract PFactory {\n    address public c0;\n    address public c1;\n    address public requester;\n\n    function set(address a, address b, address r) external {\n        (c0, c1) = a < b ? (a, b) : (b, a);\n        requester = r;\n    }\n\n    function positionOf(uint64)\n        external\n        view\n        returns (ILaunchFactory.PoolKey memory, int24, int24, address)\n    {\n        return (ILaunchFactory.PoolKey(c0, c1, 10_000, 200, address(0)), int24(-887_200), int24(887_200), requester);\n    }\n\n    function claimFees(uint64) external {}\n}\n\nabstract contract ProofBase is Test {\n    SIMDVault internal vault;\n    PToken internal imd = PToken(Fixed.IMD);\n    PToken internal token;\n    PFactory internal factory = PFactory(Fixed.LAUNCH_FACTORY);\n\n    function setUp() public virtual {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.roll(1_000_000);\n        vm.etch(Fixed.IMD, address(new PToken()).code);\n        vm.etch(Fixed.INTAKE, address(new PIntake()).code);\n        vm.etch(Fixed.LAUNCH_FACTORY, address(new PFactory()).code);\n        token = new PToken();\n        vault = new SIMDVault();\n        imd.mint(address(vault), 100 ether);\n        factory.set(address(token), Fixed.IMD, address(vault));\n    }\n}\n\n/// @notice One holding of 1,000,000 tokens opens a job from every wallet it passes through in the\n/// same block, so the per-wallet 24-hour limit does not limit jobs per holding.\ncontract JobsRecyclingProof is ProofBase {\n    function test_oneHoldingCannotOpenAJobFromEveryWalletItPassesThrough() public {\n        vm.prank(Fixed.DEV_WALLET);\n        vault.bindLaunch(1, address(token));\n        vault.distribute();\n        JobsVault jobs = vault.jobsVault();\n        address a = makeAddr(\"a\");\n        token.mint(a, 1_000_000 ether);\n        uint256 before = imd.balanceOf(address(jobs));\n\n        address holder = a;\n        for (uint256 i; i < 10; ++i) {\n            vm.prank(holder);\n            try jobs.requestJob('{\"objective\":\"x\"}') {} catch {}\n            address next = address(uint160(0x1000 + i));\n            vm.prank(holder);\n            token.transfer(next, 1_000_000 ether);\n            holder = next;\n        }\n        // Expected: the one holding pays for at most one job in this block.\n        assertLe(before - imd.balanceOf(address(jobs)), 0.5 ether, \"one holding opened many jobs\");\n    }\n}","reproduction":"Bind the launch, distribute (jobs vault receives 25.72 IMD at a 100 IMD vault balance). Mint 1,000,000e18 TEST to wallet A. Loop 10 times: current wallet calls requestJob, then transfers the 1,000,000e18 TEST to a fresh wallet. Expected: at most one job (0.5 IMD) is paid for the one holding in that block. Actual: 10 jobs open and the jobs vault pays 5 IMD.","severity":"medium","title":"JobsVault's 24-hour limit is per wallet, so one 1,000,000-token holding (or a flash-borrowed one) opens a job from every wallet it passes through and can drain the vault in one transaction"},{"description":"distribute() calls holderRewards.deposit(), which calls Intake.request(oracle.request) whenever priceOf is non-zero. Only the factory's claimFees is isolated; a revert in the oracle ask (the Intake paused, a body or asset rule the live Intake adds, an approval change) reverts distribute() as a whole. Every address is a constant and there is no other exit, so while the ask reverts the vault's IMD, the dev and marketing shares, the burn and the native ETH all stay frozen. The 'held for the next deposit' path in deposit() already exists for price == 0; a try/catch around the request (keeping the IMD free for the next epoch) would cover a reverting ask the same way.","line":133,"path":"src/SIMDVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {Fixed} from \"src/Fixed.sol\";\nimport {SIMDVault} from \"src/SIMDVault.sol\";\nimport {JobsVault} from \"src/JobsVault.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\nimport {ILaunchFactory} from \"src/interfaces/ILaunchFactory.sol\";\n\ncontract PToken is ERC20 {\n    constructor() ERC20(\"T\", \"T\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Stand-in Intake: sells every action at 0.5 IMD and pulls exactly the amount.\ncontract PIntake {\n    uint256 public nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function request(bytes32 action, bytes calldata, IIntake.Callback calldata, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32)\n    {\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        return keccak256(abi.encode(++nonce, msg.sender, action));\n    }\n}\n\n/// @dev Stand-in factory: one position whose pool is (c0, c1) and whose requester is set by the test.\ncontract PFactory {\n    address public c0;\n    address public c1;\n    address public requester;\n\n    function set(address a, address b, address r) external {\n        (c0, c1) = a < b ? (a, b) : (b, a);\n        requester = r;\n    }\n\n    function positionOf(uint64)\n        external\n        view\n        returns (ILaunchFactory.PoolKey memory, int24, int24, address)\n    {\n        return (ILaunchFactory.PoolKey(c0, c1, 10_000, 200, address(0)), int24(-887_200), int24(887_200), requester);\n    }\n\n    function claimFees(uint64) external {}\n}\n\nabstract contract ProofBase is Test {\n    SIMDVault internal vault;\n    PToken internal imd = PToken(Fixed.IMD);\n    PToken internal token;\n    PFactory internal factory = PFactory(Fixed.LAUNCH_FACTORY);\n\n    function setUp() public virtual {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.roll(1_000_000);\n        vm.etch(Fixed.IMD, address(new PToken()).code);\n        vm.etch(Fixed.INTAKE, address(new PIntake()).code);\n        vm.etch(Fixed.LAUNCH_FACTORY, address(new PFactory()).code);\n        token = new PToken();\n        vault = new SIMDVault();\n        imd.mint(address(vault), 100 ether);\n        factory.set(address(token), Fixed.IMD, address(vault));\n    }\n}\n\n/// @notice A revert in HolderRewards' oracle ask (Intake.request) blocks the whole distribution.\ncontract OracleAskBlocksProof is ProofBase {\n    function test_aRevertingOracleAskDoesNotBlockTheSplitTheBurnOrTheEth() public {\n        vm.prank(Fixed.DEV_WALLET);\n        vault.bindLaunch(1, address(token));\n        token.mint(address(vault), 5 ether);\n        vm.deal(address(vault), 1 ether);\n        // The Intake still prices oracle.request but refuses this ask (paused, body refused, ...).\n        vm.mockCallRevert(\n            Fixed.INTAKE,\n            abi.encodeWithSelector(IIntake.request.selector, Fixed.ORACLE_REQUEST),\n            abi.encodeWithSignature(\"Error(string)\", \"paused\")\n        );\n        uint256 devBefore = imd.balanceOf(Fixed.DEV_WALLET);\n        vault.distribute();\n        assertEq(imd.balanceOf(address(vault.jobsVault())), 100 ether * 2572 / 10_000);\n        assertEq(imd.balanceOf(Fixed.DEV_WALLET) - devBefore, 2 * (100 ether * 1714 / 10_000));\n        assertEq(token.balanceOf(Fixed.BURN), 5 ether);\n        assertEq(address(vault).balance, 0);\n    }\n}","reproduction":"Bind, give the vault 100 IMD, 5 TEST and 1 ETH, make Intake.request revert for oracle.request while priceOf still returns 0.5 IMD (vm.mockCallRevert). Expected: distribute() pays 25.72 IMD to JobsVault, 2 x 17.14 IMD to the dev/marketing wallet, burns 5 TEST and sends 1 ETH, holding the 40% in HolderRewards. Actual: distribute() reverts with the Intake's error and nothing moves.","severity":"low","title":"A failed oracle ask in HolderRewards.deposit reverts distribute(), freezing the whole split, the burn and the native ETH"},{"description":"bindLaunch only checks that token_ is currency0 or currency1 of the launch pool. The pool is TEST/IMD, so the dev wallet can bind IMD itself, once and irreversibly. distribute() would then 'burn' IMD left after the split, HolderRewards would ask the oracle for IMD holders and JobsVault would require 1,000,000 IMD. It is a dev-wallet-only mistake, but nothing can undo it.","line":105,"path":"src/SIMDVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {Fixed} from \"src/Fixed.sol\";\nimport {SIMDVault} from \"src/SIMDVault.sol\";\nimport {JobsVault} from \"src/JobsVault.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\nimport {ILaunchFactory} from \"src/interfaces/ILaunchFactory.sol\";\n\ncontract PToken is ERC20 {\n    constructor() ERC20(\"T\", \"T\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev Stand-in Intake: sells every action at 0.5 IMD and pulls exactly the amount.\ncontract PIntake {\n    uint256 public nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function request(bytes32 action, bytes calldata, IIntake.Callback calldata, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32)\n    {\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        return keccak256(abi.encode(++nonce, msg.sender, action));\n    }\n}\n\n/// @dev Stand-in factory: one position whose pool is (c0, c1) and whose requester is set by the test.\ncontract PFactory {\n    address public c0;\n    address public c1;\n    address public requester;\n\n    function set(address a, address b, address r) external {\n        (c0, c1) = a < b ? (a, b) : (b, a);\n        requester = r;\n    }\n\n    function positionOf(uint64)\n        external\n        view\n        returns (ILaunchFactory.PoolKey memory, int24, int24, address)\n    {\n        return (ILaunchFactory.PoolKey(c0, c1, 10_000, 200, address(0)), int24(-887_200), int24(887_200), requester);\n    }\n\n    function claimFees(uint64) external {}\n}\n\nabstract contract ProofBase is Test {\n    SIMDVault internal vault;\n    PToken internal imd = PToken(Fixed.IMD);\n    PToken internal token;\n    PFactory internal factory = PFactory(Fixed.LAUNCH_FACTORY);\n\n    function setUp() public virtual {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.roll(1_000_000);\n        vm.etch(Fixed.IMD, address(new PToken()).code);\n        vm.etch(Fixed.INTAKE, address(new PIntake()).code);\n        vm.etch(Fixed.LAUNCH_FACTORY, address(new PFactory()).code);\n        token = new PToken();\n        vault = new SIMDVault();\n        imd.mint(address(vault), 100 ether);\n        factory.set(address(token), Fixed.IMD, address(vault));\n    }\n}\n\n/// @notice bindLaunch accepts IMD, the pool's other currency, as the launched token.\ncontract BindImdProof is ProofBase {\n    function test_bindLaunchRefusesIMDAsTheLaunchedToken() public {\n        vm.prank(Fixed.DEV_WALLET);\n        vm.expectRevert();\n        vault.bindLaunch(1, Fixed.IMD);\n    }\n}","reproduction":"Factory position for launch 1 has currencies (TEST, IMD) and requester = vault. Dev wallet calls bindLaunch(1, IMD). Expected: revert (IMD is not the launched token). Actual: succeeds; vault.token() == IMD and both children are bound to IMD.","severity":"low","title":"bindLaunch accepts IMD, the pool's other currency, as the launched token"},{"description":"rollover adds pot - claimed to epochs[current].pot, and claim pays pot * balance / total at claim time. Holders of the current epoch who claimed before the rollover were paid from the smaller pot, later claimers from the larger one, and the early claimers' share of the rolled amount stays unclaimed until that epoch is itself rolled over. The pot cap still holds (the invariant suite checks claimed <= pot and reserved == sum of open pot - claimed), so nothing is overpaid or lost; it is a fairness issue.","line":215,"path":"src/HolderRewards.sol","reproduction":"Epoch 2 pot P, holders A and B with equal balances. A claims P/2. Epoch 1 (older than 28 days) is rolled over and moves R into epoch 2. B claims (P + R)/2. Expected (equal shares): A and B each receive (P + R)/2. Actual: A got P/2, B gets (P + R)/2, and R/2 waits for a later rollover.","severity":"info","title":"Rolling an epoch into a current epoch whose claims have started pays its holders unequal per-token amounts"}],"hash":"9610774c1f9187877616924184ab12cbc7589be42b1782970f5cb730f62819d1","nodeId":"6cb2e402-c7eb-4eac-bf2a-408ae9f3e314","outcome":"completed","summary":"I added an invariant suite, live fork tests and edge-case tests. `forge build` succeeds, and `forge test` with the verifier's fuzz seed passes: 71 passed, 0 failed, 9 skipped. The 9 skipped are the fork tests, which skip cleanly when no RPC URL is set. I also found four defects; they are in `.imd-findings.json` rather than in tests that would pass over them.\n\n**What I added (only under `test/`)**\n- **`test/invariant/SystemInvariant.t.sol`** — the invariant tests. A handler calls everything in random order: distribute (fee claims that pay or fail, plus native ETH), sending ETH and IMD in, oracle answers (some with a figure lower than the true total), claims (including a claim with an inflated balance), rollovers, jobs, and calls by outsiders who aren't allowed. It runs 256 sequences of 40 calls each. After every sequence it checks:\n  - every distribute matched the 40 / 25.72 / 17.14 / 17.14 split, with at most 3 wei of dust left;\n  - distribute never ran twice within 7 days;\n  - no epoch paid more than its pot, and a closed epoch never reopened;\n  - the IMD the rewards contract still owes equals what's left in its open epochs, and it holds at least that much;\n  - IMD only ever reaches the addresses the brief names;\n  - every launched token received was burned, and all native ETH reached the marketing wallet;\n  - only the dev wallet can launch or bind, and only once; only the vault can set tokens or deposit; only the Intake can deliver answers.\n  \n  A scripted test confirms the handler really reaches claims, rollovers, jobs and burns, so the invariants aren't passing on an empty system.\n- **`test/ForkLimits.t.sol`** — five more fork tests against the real Intake and launch factory, alongside the four already there. They cover dev-only and once-only launch and bind, rejecting a launch the vault didn't request, the split plus burn plus ETH to marketing, the 7-day limit, only the Intake delivering answers, and the job holding and daily limits. I ran all nine against live Robinhood Chain and they pass. Only the factory's `positionOf` is faked, because no real launch names a freshly deployed vault as its requester.\n- **`test/EdgeCases.t.sol`** — offline tests for:\n  - a token with 6 decimals, and one with no decimals;\n  - an empty week;\n  - an answer that arrives after its epoch was rolled over;\n  - a proof from one epoch tried in another;\n  - one epoch's answer given for another epoch's request;\n  - a rollover one second early;\n  - two fuzz tests (1,000 runs each) on the split and the pro-rata payouts.\n\n**Defects reported in `.imd-findings.json`** (the first three include a self-contained test that fails on the current code)\n1. **Medium — the 24-hour job limit is per wallet, not per holding.** The same 1,000,000 tokens passed from wallet to wallet open a job from each one: 10 wallets in one block cost the jobs vault 5 IMD instead of 0.5. Anyone who can borrow the tokens for a single transaction could empty the jobs vault without holding any. The code follows the brief's wording, so this is partly a gap in the brief. One existing test, `test_movingTokensDoesNotResetTheClock`, currently treats the wallet-to-wallet hand-off as correct and will need to change with any fix.\n2. **Low — a failed oracle request blocks the whole distribution.** If the Intake's `request` reverts while it still quotes a price (paused, or a new rule), `distribute()` reverts. All of the vault's IMD, the burn and the ETH stay stuck until the Intake accepts again, because there is no other way out.\n3. **Low — `bindLaunch` accepts IMD as the launched token.** The pool is TEST/IMD, and the check only asks that the token be one of the two, so the dev wallet could bind IMD itself, once and permanently.\n4. **Info — rollover payouts within an epoch are unequal.** When an old epoch rolls into one whose holders have already started claiming, the early claimers were paid from the smaller pot. Nothing is overpaid or lost; the difference waits for a later rollover.\n\nSome fil","treeHash":"1e67296f1637623f58e4b3000b649badd07a4fd3","usage":{"cachedInputTokens":2415799,"inputTokens":52,"model":"claude-opus-5-5","outputTokens":46266,"runtime":"claude","turns":30,"wallClockMs":478066}},{"artifacts":[],"attempt":1,"bundleHash":"fd66211c3e9f434ebc4e772f3ca2329ae8df68a795d5f432cc7d31173b986f8d","device":"d73af718d55ba788","findings":[],"hash":"a60ca1db144ba1fa0a8bceb926e9ab23d09ed780fcce72a39f645fa922cca065","nodeId":"f701b8e8-4f56-4b4f-8937-9e58aa83d987","outcome":"completed","summary":"Everything is green: 82 tests pass offline (including the reviewer's proof), the 4 fork tests pass against the live Robinhood Chain RPC, the fuzz seed the verifier uses passes, and `forge fmt --check` is clean. Sizes stay within limits (HolderRewards runtime 16,322 bytes, SIMDVault init code 29,215 bytes).\n\n**What changed**\n\n- **Rollover pro-rata (proof, medium):** each epoch now stores `claimable`, fixed to its pot when the root arrives, and claims pay from that base. Rollover still moves exactly `pot - claimed` into the current epoch, so the proof's assertion that the pot grows holds, but anything rolled into an already-rooted epoch passes through to the next epoch that opens instead of changing a share being claimed. The current epoch may also roll itself over after 28 days, so an unanswered ask is no longer stuck behind a newer epoch.\n- **Stranded shares (medium):** a permissionless dormancy exit that keeps \"no owner\". HolderRewards records a stall at the first deposit no root follows and clears it on any root; after 60 days anyone can send the held IMD and carry to the dev wallet. JobsVault falls back to the brief's literal live-balance rule while the oracle is dormant, and gains a notice-then-recover path if the job action itself is unsold for 60 days.\n- **Advisory items fixed:** minimum pot of twice the oracle price before an epoch opens, the decimals bound at 71, bindLaunch requiring the pool's other side to be IMD, and a distribute that moved nothing no longer spending the week.\n- **Documented rather than changed:** the refused-launch recovery through the factory's `setRequester`, the requirement that the launch body name an owner or be a custom-token body, and the caller-chosen snapshot block, which I disputed as the brief's own design.\n\nThe answers to all twelve findings are in `.imd-responses.json` at the repository root. The README covers the new dormancy section, the revised rollover and claim semantics, the new constants, and the updated test count.","treeHash":"36b71911a30a1e28213bafea5a2eca85318ed966","usage":{"cachedInputTokens":4596928,"inputTokens":1058,"model":"claude-fable-5-1","outputTokens":82168,"runtime":"claude","turns":99,"wallClockMs":1091205}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d0653dc91b6e2259","findings":[{"citation":"resolved","description":"Area: Access Control / Trust Gap (access x economics). The brief says 'Any wallet holding at least 1,000,000 of the launched token can call requestJob(bytes) at most once per 24 hours'. The implementation adds a second, undocumented-in-the-brief gate: the caller must also appear with >= minHolding in the Merkle tree of HolderRewards.latestRootedEpoch() and must have called proveHolding for exactly that epoch. Consequences: (1) no wallet can open a job between bindLaunch and the first oracle answer, however much it holds; (2) a wallet that buys 1,000,000 tokens after the latest snapshot is refused for up to a week plus the oracle's delivery time, and if the next epoch's answer never arrives (oracle refused the ask, panel disagreed, attestation expired after VALID_FOR_SECONDS) it is refused indefinitely while the previous snapshot stays in force; (3) if no root ever arrives (see finding 4), the 25.72% share sitting in JobsVault can never be spent by anyone and there is no withdraw. The README states this was chosen to stop one holding opening a job from many wallets in one block. That goal is legitimate, but it changes who is permitted to call the entry point relative to the approved brief and introduces a hard dependency of JobsVault on HolderRewards' oracle flow that the brief does not have. A requester decision is needed: either accept the stricter gate and amend the brief, or implement the brief's live-balance rule with an anti-sybil measure that does not depend on the oracle (for example a per-wallet first-seen delay, or allowing the live-balance path whenever latestRootedEpoch()==0 or the latest snapshot is older than N days).","line":123,"path":"src/JobsVault.sol","reproduction":"State: SIMDVault deployed, dev called launch and bindLaunch(token). JobsVault holds 10 IMD. No distribute() has happened yet (or distribute() ran but the oracle has not answered). Alice holds 1,000,000e18 launched tokens live (minted/bought). Call: vm.prank(alice); jobs.requestJob('{\"objective\":\"x\",\"skill\":\"build-website\"}'). Expected per brief: Intake.request('job.open@job-1', ...) is paid from JobsVault and a requestId returned. Actual: revert HoldingNotProven(alice, 0, 0). Verified in test/scratch/Review.t.sol::test_liveHolderCannotRequestJobWithoutSnapshot (passes on current code, i.e. the revert is confirmed). The existing suite's test_noJobsBeforeTheFirstRootArrives asserts the same refusal as intended behaviour.","severity":"medium","snippet":"        if (latest == 0 || proven != latest) revert HoldingNotProven(msg.sender, proven, latest);","title":"JobsVault.requestJob refuses every live holder of 1,000,000 tokens until an oracle root exists; access gate is stricter than the brief and makes the jobs budget depend on oracle liveness"},{"citation":"resolved","description":"Area: Trust Gap (access x economics) and Asymmetry (request without a fulfil/fail counterpart). The dev wallet's only privileged action irrevocably commits the vault after a single call, but success of that call is decided off chain: the intake-client reference states the Intake forwards the price and refunds nothing 'whatever the outcome', and a body the plane refuses (unknown key, missing fact, 16 KiB limit, admission refusal) spends the price and creates no launch. The vault registers no callback and does not use the version-2 Intake's requestWithFailure/onFailure, so it can never learn the ask failed. After a refused or failed launch: launched is true so launch() reverts AlreadyLaunched; no factory position names the vault as requester so bindLaunch reverts NotTheRequester for every launchNumber; distribute reverts NotBound; and by design there is no other exit for IMD. All IMD funded to the vault (the README asks for the whole weekly budget to be funded before launch) is locked permanently. The only recovery is a third party handing one of their own launches to the vault via the factory's setRequester, which is outside the vault's control. The brief says 'only once'; the intent is one successful launch, which is preserved by re-opening launched only when the Intake reports failure. Suggested minimal fix: ask with requestWithFailure and an onFailure callback (only the Intake may call it, keyed on launchRequestId) that clears launched and launchRequestId; alternatively allow launch() again while bound == false and the previous request is older than a generous deadline. Either needs the requester's agreement because it touches the 'only once' wording.","line":87,"path":"src/SIMDVault.sol","reproduction":"State: vault funded with 100 IMD; Intake sells launch.open at 0.5 IMD. Step 1: vm.prank(dev); vault.launch(body) -> Intake pulls 0.5 IMD, returns requestId, launched == true. Step 2 (off chain): the plane refuses the body (status 1, e.g. unexpected key) or the launch fails; no launch is created, nothing is refunded. Step 3: vm.prank(dev); vault.launch(correctedBody) -> revert AlreadyLaunched. Step 4: vm.prank(dev); vault.bindLaunch(n, token) for any n -> revert NotTheRequester(n, <someone else or 0>). Step 5: vault.distribute() -> revert NotBound. Result: 99.5 IMD in the vault cannot move by any function. Expected: a failed ask leaves the vault able to launch again (one successful launch). Verified in test/scratch/Review.t.sol::test_refusedLaunchStrandsVaultIMD.","severity":"medium","snippet":"        launched = true;\n        uint256 price = INTAKE.priceOf(LAUNCH_ACTION, address(IMD));\n        if (price == 0) revert ActionNotSold();\n        // Approved for exactly the price; the Intake pulls it in the same call.\n        IMD.forceApprove(address(INTAKE), price);\n        requestId = INTAKE.request(LAUNCH_ACTION, body, IIntake.Callback(address(0), bytes4(0)), address(IMD), price);","title":"launch() is one-shot with no on-chain failure path: a launch the plane refuses or that fails leaves launched=true forever, so bindLaunch and distribute become impossible and every IMD in the vault is "},{"citation":"resolved","description":"Area: Asymmetry (claim before vs after rollover in the same epoch) and Trust Gap (economics x asymmetry). claim() pays e.pot * balance / e.total (line 214) using the pot at claim time. rollover() adds pot - claimed of an older epoch to epochs[current].pot without recording a per-epoch base pot or any accounting for holders who have already claimed in the current epoch. Two holders with identical snapshot leaves in the same epoch therefore receive different amounts depending only on whether they claimed before or after a rollover landed. Since rollover() is permissionless and the time it becomes available (openedAt + 28 days of the older epoch) is public, anyone can order the two transactions: a holder waits for (or triggers) the rollover and then claims, taking a share of the rolled-over IMD that early claimers can never recover (they are blocked by hasClaimed). The brief promises payment 'pro rata to a weekly snapshot', which this violates for the rolled-over portion. The README acknowledges 'later claimers in that epoch get a proportionally larger share', so this is a known design consequence, but it is a material fairness break (12x in the reproduction below) that rewards delaying claims and punishes prompt ones. Minimal fix preserving the brief: have rollover credit the rolled amount to the epoch that will be opened by the next deposit (a pendingRollover that _openEpoch adds to the new pot, with reserved adjusted accordingly) so that no epoch's pot changes after its root arrives; or snapshot e.basePot at root arrival and pay from it, keeping any later additions for the next epoch.","line":234,"path":"src/HolderRewards.sol","reproduction":"State (test/scratch/Review.t.sol::test_rolloverAsymmetryBetweenEarlyAndLateClaimers): launch+bind, vault holds 100 IMD. t0: distribute() opens epoch 1 with pot1 = 39.5 IMD; the oracle never answers epoch 1. t0+7d: 10 IMD more arrive, distribute() opens epoch 2 with pot2 = 3.5 IMD; the oracle answers epoch 2 with a two-leaf tree alice:100, bob:100, figure 200. Alice claims at once: payout = 3.5e18*100/200 = 1.75 IMD. t0+28d: anyone calls rollover(1): epoch 2 pot becomes 3.5+39.5 = 43 IMD. Bob claims: payout = 43e18*100/200 = 21.4 IMD. Expected: two identical leaves in the same epoch receive identical payouts (or the rollover is shared equally). Actual: alice 1.75 IMD, bob 21.4 IMD; alice is blocked from claiming again by hasClaimed, and the 19.75 IMD difference she never receives is not hers in any later epoch unless she still holds.","severity":"medium","snippet":"        amount = e.pot - e.claimed;\n        e.closed = true;\n        epochs[current].pot += amount;","title":"rollover() grows the current epoch's pot after claims have started, so holders who already claimed receive a smaller per-token payout than holders who claim after the rollover; the 'pro rata to the sn"},{"citation":"resolved","description":"Area: Trust Gap (access x economics; liveness dependency on an external party with no on-chain recovery). The brief fixes the action strings and forbids any admin, so Fixed.sol makes ORACLE_REQUEST, JOB_OPEN, LAUNCH_OPEN and ORACLE_SIGNER constants with no setter. The intake-client reference states these are versioned ('job-1', 'oracle-1') and 'A new version (job-2) or a moved price must not need a redeploy'; the oracle-consumer reference lists the signer as owner-settable for the same reason. When the plane publishes oracle-2 and stops pricing oracle-1, INTAKE.priceOf returns 0; deposit() then takes the 'held' branch on every distribute: the IMD is pulled into HolderRewards, no epoch is opened, reserved is unchanged, and nothing in the contract can ever move that balance (claim and rollover need an epoch; there is no withdraw). The same happens if the Intake refuses the ask each week (DepositHeld) or if the signer rotates (epochs open and pay the oracle fee, but no answer ever verifies; pots roll forward week after week and are never claimable). JobsVault.requestJob reverts ActionNotSold forever once job-1 is unsold, stranding its 25.72% share. This is distinct from a single failed week (which the design handles by rollover): it is a permanent lock of 65.72% of every future distribution with no recovery path and no way for anyone to notice on chain except the DepositHeld event. Because the brief mandates both the fixed ids and the absence of an admin, fixing this is a scope decision for the requester. Options that keep 'no owner': (a) have distribute() skip the HolderRewards/JobsVault legs and send those shares to the dev/marketing wallets or keep them in the vault while priceOf(...)==0 (so IMD never enters a contract that cannot spend it); (b) a permissionless 'oracle dormant' fallback in HolderRewards that, after N consecutive held deposits, lets the held balance be distributed by the last known root; (c) accept and document the trust assumption prominently as a risk to holders.","line":135,"path":"src/HolderRewards.sol","reproduction":"State: launch+bind done, vault holds 100 IMD. Intake stops selling the oracle action: priceOf('oracle.request@oracle-1', IMD) == 0 (in the scratch test: intake.setPrice(ORACLE_REQUEST, IMD, 0)). Call distribute(): 40 IMD moves to HolderRewards, DepositHeld is emitted, currentEpoch stays 0, reserved stays 0. A week later distribute() again: another 4 IMD held, still no epoch. Any claim(1,...) or rollover(1) reverts UnknownEpoch(1). No function on HolderRewards can transfer the 44 IMD. Expected per brief: 40% of fees reach holders. Actual: 40% of all future fees are irrecoverable while the action is unsold. Verified in test/scratch/Review.t.sol::test_unsoldOracleActionStrandsHolderShare. For the signer: etch a different key behind Fixed.ORACLE_SIGNER after deployment; every onOracleResult reverts BadSignature and every epoch's pot only ever moves to the next epoch.","severity":"medium","snippet":"        if (price == 0 || free <= price || !_openEpoch(free - price, price)) emit DepositHeld(amount, free);","title":"Hard-coded action ids and oracle signer with no setter: once the Intake stops selling oracle.request@oracle-1 (or rotates the signer) every weekly 40% share is held in HolderRewards forever and cannot"},{"citation":"resolved","description":"Area: Asymmetry (the guard in _decimals does not match the arithmetic it protects). The bound 77 was chosen because 10**77 fits in uint256, but the value multiplied is 1_000_000 * 10**d (line 80), which exceeds 2**256-1 for d >= 72. Checked arithmetic reverts in setToken, which reverts bindLaunch, and because bindLaunch is the only path to bind, a launched token with 72..77 decimals could never be bound, leaving the vault permanently unable to distribute (finding 2's lock). The launched 'Test' token is expected to have 18 decimals, so this is unlikely to be hit in the planned launch; it is a correctness bug in a bound that is cheap to fix (use d <= 71, or fall back to 18 when the multiplication would overflow).","line":152,"path":"src/JobsVault.sol","reproduction":"Deploy an ERC-20 whose decimals() returns 72; make factory.positionOf(9) return a pool key containing it and IMD with requester == vault. vm.prank(dev); vault.bindLaunch(9, token) -> reverts with an arithmetic overflow panic inside JobsVault.setToken (minHolding = 1_000_000 * 10**72 > 2**256-1). Expected: either a sane minHolding or the documented fallback to 18 decimals. Verified in test/scratch/Review.t.sol::test_decimals72MakesBindRevert.","severity":"low","snippet":"            if (d <= 77) return uint8(d);","title":"JobsVault._decimals accepts decimals up to 77 but MIN_HOLDING_WHOLE_TOKENS * 10**d overflows for d >= 72, so setToken reverts and bindLaunch can never succeed for such a token"}],"hash":"a9956308d7ef54cbd4fc1ec5d084d6e185119d35c9e8edf1b6f3b9eefc424736","nodeId":"5ad324f4-5f0d-49ae-a3b6-049aab254ef0","outcome":"completed","summary":"The review is complete. The findings file is valid, every snippet matches the tree, all 13 entry points have a coverage row, and the working tree is clean apart from the findings file and my gitignored scratch test.\n\n## Outcome\n\nThe permission model itself holds. Every state-changing entry point has the guard the brief asks for: `launch` and `bindLaunch` are dev-only one-shots, both `setToken` functions accept only the deploying vault, `onOracleResult` accepts only the Intake and rejects foreign requests, and no address or rate can be changed after deployment. I found no permission bypass. The defects are at the trust-gap and asymmetry seams, where a correct guard meets an economic path with no recovery.\n\n**Findings written to `.imd-findings.json`** (four medium, one low), each with a reproduction I confirmed in a scratch test that passes against the current code:\n\n1. **JobsVault gate stricter than the brief.** A wallet holding 1,000,000 tokens live is refused until it proves a leaf in the latest oracle root. Before any root exists, no job can ever be opened. The README states this is deliberate anti-sybil design, so it needs a requester decision.\n2. **One-shot `launch()` with no failure path.** If the plane refuses the body, `launched` stays true, nothing can be bound, `distribute` reverts forever, and all IMD in the vault is stranded. The Intake's `requestWithFailure` offers the recovery hook the vault does not use.\n3. **Rollover breaks pro rata within an epoch.** `rollover` raises the current epoch's pot after some holders have claimed. In the reproduction, two identical leaves received 1.75 IMD and 21.4 IMD. Anyone can time this since `rollover` is permissionless.\n4. **Fixed action ids and signer with no setter.** Once the Intake stops selling `oracle-1`, every weekly 40% share is pulled into HolderRewards and held with no epoch, no claim and no rollover able to reach it. JobsVault's share is stranded the same way when `job-1` retires. This is mandated by the brief's \"no admin\" rule, so I flagged it as a scope decision with three design-preserving options.\n5. **Decimals bound off by six.** `_decimals` admits values up to 77 but the multiplication overflows at 72, which would make `bindLaunch` revert for such a token. Unlikely for the planned 18-decimal token.\n\n**Not reached:** the fork tests against live Robinhood Chain skip here because there is no network. Live behaviour of `claimFees` payout assets and the Intake's request-id scheme is taken from the references and the README's recorded run, not re-verified.","treeHash":null,"usage":{"cachedInputTokens":1501956,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":42117,"runtime":"claude","turns":34,"wallClockMs":717573}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"232b62e021f6f394","findings":[{"citation":"resolved","description":"The epoch's snapshot is fixed at `head() - 8` inside `_openEpoch`, which runs inside `HolderRewards.deposit`, which runs inside `SIMDVault.distribute()`. `distribute()` is permissionless and callable at any time after `lastDistributionAt + 7 days`, so the caller chooses the snapshot block relative to its own trades. The brief's guarantee ('cannot be gamed by buying just before a claim') holds, but the snapshot can be gamed by buying just before calling `distribute()`: the attacker's balance is in the root the oracle computes, the attacker claims `pot * balance / total`, and the genuine weekly holders are diluted. Nothing on chain ties the snapshot to a block the caller cannot pick. The cost to the attacker is two swaps' fees and price impact for a nine-block hold, which is small against a large pot. This is inherent to a snapshot taken at a permissionless call; a fix that keeps the brief's economics would anchor the snapshot to a block the caller does not control (for example the block in which the previous `distribute()` ran, stored in `lastDistributionAt`'s companion, or `lastDistributionAt + 7 days` resolved to a block), which is a design decision for the requester.","line":141,"path":"src/HolderRewards.sol","reproduction":"State: launch bound, epoch N-1 settled, `lastDistributionAt + 7 days` reached, the vault holding fees worth F IMD (so the next pot is about 0.4*F - 0.5). Genuine circulating supply S held by honest wallets. Attacker A: (1) in block B buys S tokens from the pool (A now holds 50% of included supply); (2) waits 8 blocks (about two seconds on Arbitrum Nitro) and in block B+8 calls `vault.distribute()`, so `snapshotBlock = B`; (3) in block B+9 sells the S tokens back. The oracle's root for block B contains leaf (A, S) and figure 2S; A calls `claim(N, S, proof)` and receives `pot * S / 2S = pot / 2`. Expected by the brief's intent: the pot goes to the week's holders; actual: half of it goes to an address that held for nine blocks and whose only cost was two swaps.","severity":"low","snippet":"        uint64 snapshotBlock = uint64(head() - WINDOW_DEPTH);","title":"Whoever calls distribute() picks the holder snapshot block, so a transient position held for nine blocks earns a full pro-rata share of the weekly pot"},{"citation":"resolved","description":"`launch()` sets `launched = true` and pays the launch price before the plane has looked at the body, and registers no callback (per the brief), so a refusal with status 1 (bad JSON, unknown key, wording screen) or a launch that ends without a result (status 2) is invisible on chain: the price is spent, no launch exists with the vault as requester, and `launch()` can never be called again. `bindLaunch` then has nothing to bind unless the dev wallet opens a launch through some other payer and hands the requester role to the vault with the factory's `setRequester`, which is possible per the intake-client reference but outside this contract and not documented in the README. The 'only once' rule is the brief's, so this is reported as a liveness gap in the brief's own design rather than a code error; the author should at least document the recovery path, or decide whether a second `launch()` should be allowed while `bound` is false.","line":85,"path":"src/SIMDVault.sol","reproduction":"State: fresh vault funded with 100 IMD, Intake selling `launch.open@launch-1` at 0.5 IMD. (1) dev wallet calls `vault.launch(body)` with a body the plane refuses (for example `{\"objective\":\"x\"}` with no `onchain` member, or any body the wording screen rejects). The Intake takes 0.5 IMD and returns a request id; `launched == true`. (2) Off chain the request completes with status 1; no launch position names the vault. (3) dev wallet calls `vault.launch(correctBody)`: reverts `AlreadyLaunched()`. (4) dev wallet calls `vault.bindLaunch(n, token)` for any n: reverts `NotTheRequester(n, <other>)`. Expected: the vault can still become a launch's requester; actual: only via a launch paid elsewhere plus `LAUNCH_FACTORY.setRequester(n, vault)` by that other payer. Local check: in the existing test base, `_launch()` twice reverts `AlreadyLaunched` (test_launchOnlyOnce), and nothing in the vault observes the outcome of the first.","severity":"low","snippet":"        if (launched) revert AlreadyLaunched();","title":"A launch body the plane refuses permanently consumes the vault's single launch() and the vault never learns it"},{"citation":"resolved","description":"The snapshot tie is the accepted fix from the earlier round (README 'Jobs are tied to the snapshot') and is not disputed. What the README does not state is the residual for the first epoch: `latestRootedEpoch` is 0 until the first answer is stored, and the first epoch's answer can fail for reasons the contract never sees (status 1 wording refusal, status 2 disagreement, an attestation delivered after `VALID_FOR_SECONDS`, an out-of-gas callback). While it is 0, every `requestJob` and `proveHolding` reverts for every wallet whatever it holds, which contradicts the brief's wording ('Any wallet holding at least 1,000,000 of the launched token can call requestJob'). The 25.72% share keeps arriving each week and JobsVault has no withdraw, so if the question is refused every week (the README itself says `POST /requests/check` was never run on it) that IMD is unreachable for good. This is a liveness consequence of the accepted design, reported so the requester can decide whether to accept it; a fallback that reopens the sybil hole is not recommended.","line":123,"path":"src/JobsVault.sol","reproduction":"Reproduced with test/scratch/JobsVaultBrief.t.sol (run locally, passes, meaning the revert happens). State: vault launched and bound, one `distribute()` done so JobsVault holds 25.72 IMD and HolderRewards opened epoch 1 and paid the oracle; no root has arrived. Alice holds 1,000,000 TEST (balance >= `minHolding`). Alice calls `jobs.requestJob('{\"objective\":\"x\",\"skill\":\"build-website\"}')`. Expected per the brief: the job opens and the Intake takes 0.5 IMD from JobsVault. Actual: revert `HoldingNotProven(alice, 0, 0)`. `canRequest(alice)` returns `(false, 0)`. If the oracle never answers (price unsold, body refused, signer rotated), this state is permanent and the IMD in JobsVault can never be spent.","severity":"low","snippet":"        if (latest == 0 || proven != latest) revert HoldingNotProven(msg.sender, proven, latest);","title":"JobsVault refuses every wallet holding 1,000,000 tokens until an oracle root exists, and its IMD has no other exit"}],"hash":"b8d43fb0e363ef45e7979aceb7dbe24eb31a7071f95fab20bb7bb37169583fe0","nodeId":"09fa4b3f-b699-4b9d-9aaf-fca380d3d221","outcome":"completed","summary":"Review complete. The findings file is written and every snippet was verified against the tree; the scratch test directory was removed and the working tree is clean apart from the findings file.\n\n**Outcome.** The three contracts hold up well under the Execution Trace, Periphery and First Principles passes. The full local suite passes (86 tests, 9 fork tests skipped offline), and the oracle callback runs in about 115k gas against the 200k stipend. I found no loss-of-funds or permission defect. Three low findings are reported, each with a concrete state and call sequence:\n\n- **Snapshot is caller-timed.** `distribute()` is permissionless and the holder snapshot is fixed eight blocks before it, so an address can buy, wait nine blocks, call `distribute()`, sell, and claim a pro-rata share of the pot. The brief's \"buy just before a claim\" guarantee holds, but the snapshot itself is gameable. Anchored at `src/HolderRewards.sol:141`.\n- **A refused launch body consumes the single `launch()`.** No callback is registered, so the vault never learns a status-1 refusal, and the only recovery is a launch paid elsewhere plus the factory's `setRequester`, which the README does not document. Anchored at `src/SIMDVault.sol:85`.\n- **JobsVault is dead until the first root arrives.** The snapshot tie is the accepted fix from the earlier round and I did not dispute it, but while no root exists every holder is refused and the 25.72% share has no other exit. If the oracle question is refused every week, that IMD is stranded. Anchored at `src/JobsVault.sol:123`, reproduced locally.\n\n**Coverage.** All 13 listed entry points have a row: 10 hold, 3 carry findings. I also recorded the reserved-versus-balance invariant, the periphery encoders, and the documented constant-signer trust assumption as holding. One row is marked unreached: the fork runs and the wording-screen check on the oracle body need network access this review did not have.\n\n**Not re-reported.** The rollover late-claimer skew, the impossibility of signer rotation, and the JobsVault snapshot gating are all documented design decisions in the README, so I left them out of the findings and noted them in coverage instead.","treeHash":null,"usage":{"cachedInputTokens":1417637,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":43779,"runtime":"claude","turns":33,"wallClockMs":663734}},{"artifacts":[],"attempt":1,"bundleHash":"657bdfd5dd4d3ded5f7c43aae545fc48ec6eaff07f7507d1920797c66c04a8c9","device":"4dd74fd7c315da80","findings":[{"description":"This is new: it comes from the fix to the earlier JobsVault finding (e2fffde7). That fix holds up: one holding moved through ten wallets now opens exactly one job (test/EdgeCases.t.sol test_oneHoldingPassedThroughTenWalletsOpensOneJob). But _requestJob now reverts HoldingNotProven whenever rewards.latestRootedEpoch() == 0, and only a HolderRewards oracle callback with a non-zero root moves it. If no answer ever arrives, no job can be opened. That happens if the plane refuses the body (status 1), the panel disagrees (status 2), a callback fails inside the 200 000 gas stipend and is not retried, or oracle.request stops being sold (deposit then holds the IMD and opens no epoch). JobsVault has no withdraw, so every week's 25.72% builds up there with no way out. Before the fix, a dependency on the oracle froze only HolderRewards' share. The author's README mentions 'until the first root arrives no job can be opened' as a residual, but does not say it can be permanent or that it locks funds. The same holds after a first root: jobs then depend on every later root arriving, or holders stay stuck on an old snapshot. That is milder, since the old snapshot still works. Possible fixes that keep the anti-recycling property: fall back to a live-balance check with a minimum holding time when no root is newer than N days, or let a proven epoch stay valid until a newer root exists (it already does). The first case, no root ever, needs the fallback.","line":124,"path":"src/JobsVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {VaultTestBase} from \"test/Base.t.sol\";\n\n// Uses the project's shared mock base (test/Base.t.sol); fails on the current code with HoldingNotProven(alice, 0, 0).\ncontract NoRootFreezesJobs is VaultTestBase {\n    function test_jobsVaultIsFrozenWhileNoOracleRootArrives() public {\n        _launchAndBind();\n        token.mint(alice, 10_000_000 ether);\n        for (uint256 w; w < 4; ++w) {\n            vault.distribute();\n            vm.warp(block.timestamp + 7 days);\n            imd.mint(address(vault), 100 ether);\n        }\n        assertGt(imd.balanceOf(address(jobs)), 90 ether);\n        vm.prank(alice);\n        jobs.requestJob('{\"objective\":\"x\"}');\n    }\n}","reproduction":"Mock setup from test/Base.t.sol. Launch and bind, mint 10,000,000e18 TEST to alice. Four times: distribute() (the Intake takes and is paid for the oracle ask, but the plane never calls onOracleResult), warp 7 days, mint 100 IMD to the vault. JobsVault now holds more than 90 IMD. alice calls jobs.requestJob('{\"objective\":\"x\"}'). Expected (brief: 'any wallet holding at least 1,000,000 of the launched token can call requestJob'): a job opens for 0.5 IMD. Actual: it reverts HoldingNotProven(alice, 0, 0). No other function can move JobsVault's IMD, so it stays locked until a root arrives, if one ever does.","severity":"low","title":"After the snapshot fix, JobsVault cannot open any job (and its IMD has no other exit) until an oracle root arrives, so a missing or refused oracle answer freezes the 25.72% share"}],"hash":"b968c624c6fbdd9a553a3b9b94480af8f5e0ac6978f2600cf07e4b1be6b27be3","nodeId":"6cb2e402-c7eb-4eac-bf2a-408ae9f3e314","outcome":"completed","summary":"I updated my three test files to match the author's fixes, checked each fix with a test of my own, and reported one new low-severity problem that the JobsVault fix introduced.\n\n**Tests:** `forge test` with the verifier's fuzz seed gives 86 passed, 9 skipped. The 9 skipped are the fork tests, which skip when no RPC URL is set. The only failure is my proof file in `test/scratch/`, which is supposed to fail and gets deleted before verification. I also ran all 9 fork tests against live Robinhood Chain (`https://rpc.mainnet.chain.robinhood.com`), using the real Intake, factory and IMD, and they passed.\n\n**Why my tests needed changing:** after the fix, a wallet has to prove its balance against the latest oracle snapshot before it can open a job. That broke three of my tests:\n- **`test/EdgeCases.t.sol`:** the 1,000,000-token threshold for a 6-decimal token is now checked twice: one unit short at the snapshot is refused, and one unit short in the live balance is refused. The \"jobs vault has no IMD\" test proves the holding first; before, it was failing for the wrong reason.\n- **`test/invariant/SystemInvariant.t.sol`:** the random-sequence tests now prove holdings against the latest root, sometimes skip the proof on purpose, and try a forged balance. I added a token-transfer action so one holding moves between wallets. A job is expected exactly when the caller proved at least 1,000,000 tokens at the snapshot, still holds that much, is past its 24 hours, and the vault can pay.\n- **`test/ForkLimits.t.sol`:** before opening a job on the live Intake, the test now delivers a snapshot from the Intake's address and proves a holding. It also covers a holder one token short being refused, the 24-hour wait, and a second job after it.\n\n**Your four findings from last round:**\n- **One holding opening jobs from many wallets (medium):** fixed. My original reproduction now opens exactly one job (0.5 IMD) for one 1,000,000-token holding moved through ten wallets.\n- **A failed oracle request freezing `distribute()` (low):** fixed. With the request made to revert, `distribute()` still pays every share, burns the tokens and sends the ETH. The 40% waits in HolderRewards with nothing left approved, and joins the next week's pot.\n- **IMD accepted as the launched token (low):** fixed. `bindLaunch` refuses IMD, nothing gets bound, and the real token still binds afterwards.\n- **Unequal payouts after a rollover (info):** I accept the author's answer. Moving `pot - claimed` into the current pot and paying each holder once per epoch is what the brief asks for, and the invariant tests confirm nothing is overpaid or lost. I didn't report it again.\n\n**New finding (low), written to `.imd-findings.json` with a test that fails on the current code:** JobsVault can't open any job until an oracle snapshot has arrived. If one never arrives, because the body is refused, the panel disagrees, the callback runs out of gas, or the oracle stops being sold, JobsVault's 25.72% share is stuck for good because it has no other way out. In the reproduction, after four weekly distributions JobsVault holds over 90 IMD, and a holder with 10,000,000 tokens is still refused (`HoldingNotProven(alice, 0, 0)`). The README calls \"no job before the first root\" a known limit, but doesn't say it can be permanent or that funds pile up.","treeHash":"0b3dce07c160b48b7aebcc2adb27b2d283eacab8","usage":{"cachedInputTokens":1527797,"inputTokens":40,"model":"claude-opus-5-5","outputTokens":21108,"runtime":"claude","turns":21,"wallClockMs":274412}},{"artifacts":[],"attempt":1,"bundleHash":"7041a7715803a05cf5ce33f384b6c43030e6d64c573cca9fccfcc95597e723d2","device":"723b11f958c65250","findings":[],"hash":"c5e5a95283a5e00a1320e0140be1beea3a89d460b3f23f23f018bba93c2b4d19","nodeId":"f701b8e8-4f56-4b4f-8937-9e58aa83d987","outcome":"completed","summary":"The work is complete and committed on `main` (commit `151ab5f`).\n\n**What was built**\n\n- `src/SIMDVault.sol`: zero-argument constructor that deploys `HolderRewards` and `JobsVault`. `launch(body)` and `bindLaunch(n, token)` are dev-wallet-only and run once; `bindLaunch` checks the factory names the vault as requester and that the token is a pool currency. `distribute()` is public, gated to 7 days, claims fees with a low-level call (a revert never blocks the split), splits the whole IMD balance 40 / 25.72 / 17.14 / 17.14, burns the launched token balance, and sends native ETH to the marketing wallet. Every address is a constant; no owner, no admin.\n- `src/HolderRewards.sol`: each weekly deposit opens an epoch, pays 0.5 IMD for an `oracle.request` with a pinned absolute-window question, and refuses any answer that is not from the Intake, not for its own request, not the pinned question hash, from a weak panel, or badly signed. Holders claim `pot * balance / total` once per epoch against a Merkle proof; the pot cap is enforced; `rollover` after 28 days moves exactly `pot - claimed` into the current epoch.\n- `src/JobsVault.sol`: `requestJob(body)` for wallets holding at least 1,000,000 launched tokens, once per 24 hours, paid from its own IMD. No withdraw.\n- Protocol libraries copied verbatim from the oracle reference; forge-std 1.9.7 and OpenZeppelin 5.5.0 vendored as plain files, no submodules.\n\n**Verification**\n\n- `forge build`, `forge test` (58 passing offline, 4 fork tests skipped without an RPC URL), and `forge fmt --check` all pass with the verifier's fuzz seed.\n- The fork tests passed here against live Robinhood Chain: the real Intake took the launch, oracle and job payments at 0.5 IMD, and `positionOf` and `poolManager()` match the interface and constants.\n- The protocol conformance vector passes (digest, callback selector, real signature). A scratch replica of the protected floor passed: init code 24.4 KB, runtimes under EIP-170, no forbidden opcodes, no `owner()`.\n\n**Assumptions to be aware of** (all documented in `README.md`)\n\n- The snapshot block is 8 blocks before the deposit, as the oracle protocol requires an already-final window.\n- The pro rata total comes from the attestation's `figure`, which the question explicitly asks for. An epoch whose answer lacks it rolls over after 28 days.\n- The oracle question was not sent to `POST /requests/check` because its base URL is not in the available references; the exact body is readable from `bodyFor(block)` and should be checked before launch.","treeHash":"1228f08f543ca543f6dd504710662f23ef75b2de","usage":{"cachedInputTokens":4458411,"inputTokens":866,"model":"claude-fable-5-1","outputTokens":99078,"runtime":"claude","turns":53,"wallClockMs":1254077}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0cf632e317dfab7a","findings":[{"citation":"resolved","description":"Merged from audit_math, audit_permissions #3 and audit_economics #4 (same root cause). claim() pays e.pot * balance / e.total from the live pot (line 214) and marks hasClaimed; rollover() adds pot - claimed of an older epoch to epochs[currentEpoch].pot at any time after the old epoch's openedAt + 28 days, which with weekly deposits is normally about a week after the current epoch's root arrived and claims started. A holder who claimed before the rollover is paid from the smaller pot and can never claim the difference (AlreadyClaimed); one who claims after gets the larger share. rollover() is permissionless, so a holder can order the two calls (wait, roll, then claim). The PotExceeded cap keeps the epoch solvent, so no overpayment; the broken guarantee is 'pro rata to the snapshot', and early claimers' share of the rolled amount drifts to a later epoch whose holder set may differ. Fix that keeps the brief's 'exactly pot - claimed moves': credit the rolled amount to the next epoch opened (a pending carry added in _openEpoch before any claim is possible), or only roll into an epoch that has no root yet.","line":236,"path":"src/HolderRewards.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Fixed} from \"src/Fixed.sol\";\nimport {HolderRewards} from \"src/HolderRewards.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ScratchERC20 {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a; balanceOf[to] += a; return true;\n    }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\ncontract ScratchIntake {\n    uint256 public nonce;\n    function priceOf(bytes32, address) external pure returns (uint256) { return 1 ether; }\n    function request(bytes32, bytes calldata, IIntake.Callback calldata, address asset, uint256 amount)\n        external payable returns (bytes32 id)\n    {\n        ScratchERC20(asset).transferFrom(msg.sender, address(this), amount);\n        id = keccak256(abi.encode(++nonce));\n    }\n}\n\ncontract ScratchSigner {\n    function isValidSignature(bytes32, bytes calldata) external pure returns (bytes4) { return 0x1626ba7e; }\n}\n\n/// A rollover into an epoch that is already being claimed changes the per-token rate mid-epoch:\n/// holders who claimed before it get pot_old * b / total, those after get (pot_old + rolled) * b / total.\ncontract RolloverProRataTest is Test {\n    HolderRewards rewards;\n    ScratchERC20 imd = ScratchERC20(Fixed.IMD);\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n\n    function setUp() public {\n        vm.warp(1_800_000_000);\n        vm.roll(1_000);\n        vm.etch(Fixed.IMD, address(new ScratchERC20()).code);\n        vm.etch(Fixed.INTAKE, address(new ScratchIntake()).code);\n        vm.etch(Fixed.ORACLE_SIGNER, address(new ScratchSigner()).code);\n        rewards = new HolderRewards(Fixed.ORACLE_SIGNER); // this test plays the vault\n        rewards.setToken(address(0xBEEF), address(0xCAFE));\n        imd.mint(address(this), 1_000 ether);\n        imd.approve(address(rewards), type(uint256).max);\n    }\n\n    function _deliver(uint256 epoch, bytes32 requestId, bytes32 root, uint256 total) internal {\n        HolderRewards.Epoch memory e = rewards.epochInfo(epoch);\n        OracleAttestation.Attestation memory a = OracleAttestation.Attestation({\n            requestId: keccak256(abi.encode(\"req\", epoch)), chainId: block.chainid, questionHash: e.questionHash,\n            answerType: OracleAttestation.ANSWER_BYTES32, answer: abi.encode(root), figure: total,\n            fromBlock: e.snapshotBlock, toBlock: e.snapshotBlock, blockHash: bytes32(0), panelJobId: bytes32(0),\n            panelSize: 5, quorum: 3, agreed: 5, issuedAt: uint64(block.timestamp),\n            expiresAt: uint64(block.timestamp + 1 days)\n        });\n        vm.prank(Fixed.INTAKE);\n        rewards.onOracleResult(requestId, a, \"\");\n    }\n\n    function test_holdersWithEqualBalancesGetEqualPayoutsAcrossARollover() public {\n        bytes32 la = keccak256(bytes.concat(keccak256(abi.encode(alice, uint256(50)))));\n        bytes32 lb = keccak256(bytes.concat(keccak256(abi.encode(bob, uint256(50)))));\n        bytes32 root = la < lb ? keccak256(abi.encodePacked(la, lb)) : keccak256(abi.encodePacked(lb, la));\n        bytes32[] memory pa = new bytes32[](1); pa[0] = lb;\n        bytes32[] memory pb = new bytes32[](1); pb[0] = la;\n\n        // Week 0: epoch 1, pot 100 IMD; nobody claims it.\n        rewards.deposit(101 ether);\n        // Day 21: epoch 2, pot 100 IMD; alice and bob each hold half of the snapshot.\n        vm.warp(block.timestamp + 21 days);\n        vm.roll(block.number + 100);\n        rewards.deposit(101 ether);\n        _deliver(2, keccak256(abi.encode(uint256(2))), root, 100);\n        assertEq(rewards.epochInfo(2).pot, 100 ether);\n\n        vm.prank(alice);\n        uint256 alicePaid = rewards.claim(2, 50, pa); // 50 IMD\n\n        // Day 28: anyone rolls epoch 1's unclaimed 100 IMD into epoch 2.\n        vm.warp(block.timestamp + 7 days);\n        rewards.rollover(1);\n        assertEq(rewards.epochInfo(2).pot, 200 ether);\n\n        vm.prank(bob);\n        uint256 bobPaid = rewards.claim(2, 50, pb); // 100 IMD\n\n        // Same snapshot balance, same epoch: the pro-rata share should be equal.\n        assertEq(alicePaid, bobPaid, \"equal snapshot balances paid unequally\");\n    }\n}","reproduction":"Attached proof test/scratch (Proof_54274a9b11aa): HolderRewards alone, price 1 IMD. t0: deposit(101e18) opens epoch 1 (pot 100e18), no root. t0+21d: deposit(101e18) opens epoch 2 (pot 100e18), root with leaves (alice,50),(bob,50), figure 100. alice.claim(2,50,proof) -> 50e18. t0+28d: rollover(1) -> epochInfo(2).pot == 200e18. bob.claim(2,50,proof) -> 100e18. Expected: equal leaves in the same epoch receive equal payouts (or the rollover does not change an epoch already being claimed). Actual: alice 50e18, bob 100e18; the test fails with '50000000000000000000 != 100000000000000000000'. Run: forge test --match-path test/scratch/Proof_rollover.t.sol.","severity":"medium","snippet":"        epochs[current].pot += amount;","title":"rollover() grows the current epoch's pot after claims began, so equal snapshot balances are paid unequally"},{"citation":"resolved","description":"Merged from audit_permissions #4 and audit_economics #2. The brief fixes the action strings and forbids any admin, so Fixed.ORACLE_REQUEST, Fixed.JOB_OPEN and Fixed.ORACLE_SIGNER are constants with no setter; the oracle-consumer and intake-client references say these are versioned and 'must not need a redeploy'. Once the Intake prices oracle.request@oracle-1 at 0 (or refuses the ask every week), deposit() takes the held branch at line 135 on every distribute: the 40% is pulled into HolderRewards, no epoch opens, and no function can ever move it (claim and rollover need an epoch; there is no withdraw). If instead the signer key rotates, every answer fails BadSignature and every epoch's pot only ever hops to the next epoch. JobsVault.requestJob reverts ActionNotSold once job-1 is unsold and its 25.72% accumulates with no exit. SIMDVault keeps routing 65.72% of every week's fees into the two dead contracts. This is a requester decision because the brief mandates both constants and 'no admin'; options that keep 'no owner': have SIMDVault.distribute() keep the holders'/jobs' legs in the vault (or route them to the dev/marketing wallets) while INTAKE.priceOf(action, IMD) == 0, or a permissionless dormant-mode fallback after N consecutive held deposits; at minimum document the trust assumption to holders.","line":135,"path":"src/HolderRewards.sol","reproduction":"test/scratch/Review.t.sol::test_unsoldOracleActionStrandsHolderShare (passes = behaviour confirmed): launch+bind, vault holds 99.5 IMD. intake.setPrice(ORACLE_REQUEST, IMD, 0). distribute(): HolderRewards balance 39.8 IMD, DepositHeld emitted, currentEpoch == 0, reserved == 0; rollover(1) and claim(1, ...) revert UnknownEpoch(1). JobsVault holds 25.5914 IMD; with JOB_OPEN priced 0 requestJob reverts ActionNotSold. Expected per brief: 40% reaches holders and 25.72% pays jobs. Actual: both shares are unreachable by any function while the actions are unsold, and every later distribute adds to them.","severity":"medium","snippet":"        if (price == 0 || free <= price || !_openEpoch(free - price, price)) emit DepositHeld(amount, free);","title":"Fixed action ids and signer with no exit: a retired oracle.request@oracle-1 / job.open@job-1 or a rotated signer strands 65.72% of every later week's fees for good"},{"citation":"resolved","description":"Merged from audit_permissions #2, audit_economics #1 and audit_flow #2. launched is set and the price pulled before the plane reads the body; request (no callback, as the brief says) never reports status 1/2, so a refused body leaves launched == true, no position naming the vault, bindLaunch reverting NotTheRequester and distribute reverting NotBound. Severity is low, not medium: the vault's IMD is not permanently lost, because the dev wallet can pay for the launch from another payer and hand the fee share to the vault with LAUNCH_FACTORY.setRequester(launchNumber, vault) (intake-client reference), after which bindLaunch and distribute work; the README also tells the operator to run POST /requests/check first. The author should document that recovery path, or (requester decision, since it touches 'only once' and 'no callback') ask with requestWithFailure and an Intake-only failure hook that clears launched.","line":87,"path":"src/SIMDVault.sol","reproduction":"test/scratch/Review.t.sol::test_refusedLaunchLeavesVaultUnableToRelaunch: position 7 names alice, not the vault (a refused launch creates none). dev: vault.launch(body) -> 0.5 IMD spent, launched == true. dev: launch(body) again -> AlreadyLaunched. dev: bindLaunch(7, token) -> NotTheRequester(7, alice). distribute() -> NotBound. After the position's requester becomes the vault (setRequester), bindLaunch and distribute succeed and the 99.5 IMD is split. Expected: a refusal does not consume the single launch; actual: it does, and only the external setRequester path recovers.","severity":"low","snippet":"        launched = true;","title":"launch() is spent and locked on its first call even when the plane refuses the body; recovery is only through the factory's setRequester from another payer"},{"citation":"resolved","description":"Merged from audit_economics #3 and audit_flow #1. _openEpoch fixes snapshotBlock = head() - WINDOW_DEPTH inside SIMDVault.distribute(), which anyone may call at any time once lastDistributionAt + 7 days has passed. The brief's literal guarantee (no gaming by buying before a claim) holds, but a buyer can buy TEST, call distribute 8 blocks later (seconds on an Arbitrum Orbit chain), sell in the next block, and be in the root at full weight, diluting week-long holders. Cost is two swaps' fees and price impact; profitable when 0.4 x weekly fees x f/(1+f) exceeds that. This follows from the brief's own 'balance at the deposit block' and permissionless distribute, so it is a design decision for the requester; a fix that keeps the economics anchors the snapshot to a block the caller cannot choose (for example the block recorded at the previous distribute).","line":141,"path":"src/HolderRewards.sol","reproduction":"test/scratch/Review.t.sol::test_snapshotIsEightBlocksBeforeCallerChosenDistribute: launch+bind; ArbSys head 5_000_000; alice calls distribute(); epochInfo(1).snapshotBlock == 4_999_992. So a swap by alice at block 4_999_992 and a sale at 5_000_001 is counted at full balance in the epoch's root, and alice claims pot * balance / total. Expected by the brief's intent: the week's holders are paid; actual: a nine-block holding is paid the same as a week-long one.","severity":"low","snippet":"        uint64 snapshotBlock = uint64(head() - WINDOW_DEPTH);","title":"The holder snapshot block is 8 blocks before whichever block the permissionless distribute() caller picks, so a position held for nine blocks earns a full week's share"},{"citation":"resolved","description":"Merged from audit_permissions #1 and audit_flow #3. The snapshot tie (prove the leaf in HolderRewards.latestRootedEpoch(), then still hold live) was accepted in the earlier round to stop one holding opening jobs from many wallets, and is not disputed. The residual the brief's wording ('any wallet holding at least 1,000,000') does not cover: between bindLaunch and the first stored root nobody can open a job whatever they hold; a wallet that buys after the latest snapshot waits up to a week plus delivery; and if no root ever arrives (refused question, panel disagreement, expired attestation, signer rotation) the 25.72% share is unreachable because there is no withdraw. Requester decision: keep the gate and amend the brief's wording, and consider whether a root-less period longer than N weeks should fall back.","line":123,"path":"src/JobsVault.sol","reproduction":"test/scratch/Review.t.sol::test_liveHolderRefusedBeforeFirstRoot: launch+bind, distribute() once (epoch 1 opened, JobsVault holds 25.5914 IMD, no root). token.mint(alice, 1_000_000e18). alice: jobs.requestJob('{\"objective\":\"x\",\"skill\":\"build-website\"}'). Expected per brief: Intake.request(job.open@job-1) paid from JobsVault. Actual: revert HoldingNotProven(alice, 0, 0); canRequest(alice) == (false, 0).","severity":"low","snippet":"        if (latest == 0 || proven != latest) revert HoldingNotProven(msg.sender, proven, latest);","title":"JobsVault refuses every live holder of 1,000,000 tokens until an oracle root exists; its IMD depends on the oracle flow and has no other exit"},{"citation":"resolved","description":"From audit_permissions #5, reproduced. The bound 77 fits 10**d in uint256 but the product at line 80 is MIN_HOLDING_WHOLE_TOKENS * 10**d = 10**(d+6), which exceeds 2**256-1 (about 1.158e77) for d >= 72. Checked arithmetic reverts in setToken, bindLaunch reverts, and because bindLaunch is the only way to bind, such a token could never be bound. The planned 'Test' token has 18 decimals, so this is a latent bound mismatch; use d <= 71 or fall back to 18 when the product would overflow.","line":152,"path":"src/JobsVault.sol","reproduction":"test/scratch/Review.t.sol::test_decimals72MakesBindRevert: an ERC-20 whose decimals() returns 72; factory.positionOf(9) = (token, IMD, requester = vault). dev: vault.bindLaunch(9, token). Expected: bound with a sane minHolding or the documented 18-decimal fallback. Actual: arithmetic overflow panic inside JobsVault.setToken.","severity":"low","snippet":"            if (d <= 77) return uint8(d);","title":"JobsVault._decimals accepts decimals up to 77 but 1_000_000 * 10**d overflows for d >= 72, so setToken and therefore bindLaunch revert for such a token"},{"citation":"resolved","description":"From audit_economics #5, reproduced. The only threshold is free > price. A thin week opens an epoch whose pot is a few wei while 0.5 IMD goes to the Intake, and the dust mostly cannot be claimed (NothingToClaim) and rolls over 28 days later. Because distribute() is permissionless, the caller rather than the holders decides whether a thin week is cut at full oracle cost. Over a long low-volume period the holders' 40% is converted largely into oracle fees. Fix: hold the deposit unless free >= K * price or free - price exceeds a fixed minimum pot; held IMD already carries into the next epoch.","line":134,"path":"src/HolderRewards.sol","reproduction":"test/scratch/Review.t.sol::test_dustPotSpendsFullOraclePrice: bound; vault IMD set to 1.26 IMD; oracle price 0.5 IMD. distribute(): toHolders = 0.504 IMD; free = 0.504 > 0.5 so _openEpoch(0.004 IMD, 0.5 IMD) runs; epochInfo(1).pot == 0.004 IMD and the Intake holds 1 IMD (launch + oracle). Expected: a pot smaller than its oracle fee is held for a bigger week; actual: 99.2% of the holders' share is spent on the ask.","severity":"low","snippet":"        uint256 free = IMD.balanceOf(address(this)) - reserved;","title":"No minimum pot: an epoch is opened and the full oracle price spent even when the pot is dust"},{"citation":"resolved","description":"From audit_economics #6, reproduced. lastDistributionAt is advanced before and regardless of the outcome of the low-level claimFees call. If the factory reverts at that moment, the vault splits what it holds (often nothing) and the claimable fees stay in the factory for at least 7 more days; anyone can time the call to a failing moment. Bounded delay, no loss, factory trusted: low. Within the brief: only advance lastDistributionAt when claimed is true or total > 0.","line":122,"path":"src/SIMDVault.sol","reproduction":"test/scratch/Review.t.sol::test_failedClaimStillConsumesTheWeek: bound; vault holds 0.5 IMD; factory configured to pay 100 IMD but claimFees reverts. distribute(): FeesClaimed(7, false), lastDistributionAt = now. Factory fixed; one second later distribute() reverts TooEarly(now + 7 days). Expected: a distribution that moved nothing does not spend the slot; actual: 100 IMD of fees wait a week.","severity":"low","snippet":"        lastDistributionAt = block.timestamp;","title":"A distribute() whose claimFees fails still consumes the 7-day slot, delaying that week's fees by a full period"},{"citation":"resolved","description":"From audit_economics #7, reproduced. The ask uses request with no onFailure, so status 1/2 or an attestation delivered after expiresAt (AttestationExpired reverts the callback, and the Intake does not retry) leaves the epoch rootless with no on-chain signal. Its pot is unreachable until openedAt + 28 days, and rollover also needs a newer epoch (NoNewerEpoch). With requestWithFailure and an Intake-only onOracleFailure the pot could fold into the next weekly epoch instead. Design consequence of using plain request; requester decision.","line":147,"path":"src/HolderRewards.sol","reproduction":"test/scratch/Review.t.sol::test_rootlessEpochWaits28DaysAndANewerEpoch: bound; distribute() opens epoch 1 at day 0, no answer. Day 27: rollover(1) reverts NoNewerEpoch(1) (epoch 1 is still current). Any claim(1, ...) reverts NoRoot(1). Only after a later distribute opens epoch 2 and day 28 passes can rollover(1) move the pot. Expected: a failed ask is recognised and the pot moves on at the next weekly epoch; actual: frozen for 28 days at least.","severity":"low","snippet":"        try INTAKE.request(","title":"A refused, disagreed or late oracle answer leaves the epoch's pot frozen until rollover at 28 days (and only once a newer epoch exists); the Intake's failure hook is not used"},{"citation":"resolved","description":"From audit_economics #8. Trust assumption on the dev wallet rather than a bypass. If the launch body produced a native-ETH pool (currency0 == address(0)), the check passes, every claimFees pays ETH plus TEST, 100% of the ETH goes to the marketing wallet and nothing reaches holders or jobs, for good. One extra check (one of key.currency0 / key.currency1 must be IMD) makes the brief's economics a contract guarantee.","line":107,"path":"src/SIMDVault.sol","reproduction":"factory.positionOf(n) returns key {currency0: address(0), currency1: TEST}, requester == vault; dev calls bindLaunch(n, TEST): succeeds. Every distribute(): IMD split 0, ETH -> marketing, TEST burned. Expected (brief): TEST/IMD pool with 40% of fees to holders; actual: holders and jobs get nothing.","severity":"info","snippet":"        if (key.currency0 != token_ && key.currency1 != token_) revert TokenNotInPool(launchNumber_, token_);","title":"bindLaunch checks the token is in the pool but not that the other currency is IMD, so a TEST/ETH pool would route all fee value past HolderRewards and JobsVault"},{"citation":"resolved","description":"Reviewer note from the intake-client reference ('every $owner constructor argument is the body's owner, else the payer'). The launch body is the dev wallet's input, so this is operational, not a code defect: a custom_token body (as in the tests) refuses owner and sends tokens to economics.remainderTo, which is fine; but a body that deploys ownable application contracts without an explicit owner would leave them owned by SIMDVault, which has no function to forward owner-only calls. Document that the launch body must name owner (lowercase hex) or be a custom_token body.","line":92,"path":"src/SIMDVault.sol","reproduction":"dev calls vault.launch(body) with an evm_project/univ4_hook-style body lacking 'owner'. The plane deploys the app contracts with $owner = payer = SIMDVault. Expected: the dev wallet owns them; actual: SIMDVault owns them and nothing in the vault can call them.","severity":"info","snippet":"        requestId = INTAKE.request(LAUNCH_ACTION, body, IIntake.Callback(address(0), bytes4(0)), address(IMD), price);","title":"The vault is the payer of launch.open, so a body that does not name an owner makes the vault the $owner of any ownable launch contract, which it cannot operate"}],"hash":"fd8bbf1d1a0ad0195ef5a175c489cbcbbd1481f2132cec74cfb511938698754d","nodeId":"dffc976e-8117-4020-a9c3-a00c92fb837e","outcome":"completed","summary":"The review is complete. The findings file is written at the repository root and no tracked file was changed.\n\n**What I did**\n\n- Built the project and ran the full suite: 70 offline tests pass, the 9 fork tests skip without an RPC URL.\n- Ran the attached rollover proof. It fails on the current code as stated, with alice paid 50 IMD and bob 100 IMD for equal leaves.\n- Wrote 8 reproductions of my own under `test/scratch/Review.t.sol`, each asserting the reported behaviour against the project's mocks. All 8 confirm.\n- Checked the manifest: schema keys only, one zero-argument contract, notes under 4,000 characters, sizes under the limits.\n\n**Findings kept (11), after merging 17 specialist reports**\n\n| # | Severity | Finding |\n|---|---|---|\n| 1 | medium | `rollover()` grows an epoch's pot after claims began, so equal snapshot balances are paid unequally. Three specialists reported this. Proof attached. |\n| 2 | medium | Fixed action ids and signer with no exit. A retired `oracle-1` or `job-1` action, or a rotated signer, strands 65.72% of every later week's fees. Requester decision, since the brief mandates no admin. |\n| 3 | low | A refused launch body consumes the single `launch()`. Downgraded from medium because the factory's `setRequester` from another payer recovers the vault. |\n| 4 | low | The snapshot block is 8 blocks before whichever block the permissionless `distribute()` caller picks. |\n| 5 | low | JobsVault refuses every live holder until the first root exists. Documented deviation from the brief, accepted in the earlier round. |\n| 6 | low | `_decimals` accepts 72..77 but the minimum-holding product overflows, so `bindLaunch` would revert for such a token. |\n| 7 | low | No minimum pot: a dust pot still pays the full oracle price. |\n| 8 | low | A failed `claimFees` still consumes the 7-day slot. |\n| 9 | low | A refused or late oracle answer freezes the pot until a 28-day rollover. |\n| 10 | info | `bindLaunch` does not check that the pool's other currency is IMD. |\n| 11 | info | The vault is the launch payer, so a body without `owner` would make it own any ownable launch contracts. My own note. |\n\n**Dropped or downgraded**\n\n- No specialist finding failed to reproduce. The two medium ratings on the refused-launch issue were lowered because a recovery path exists.\n- Slither and Aderyn leads were all benign on inspection: the ignored `positionOf` fields are unused by design, the reentrancy flags are on trusted calls after state is set, and the ETH send goes to a constant address.\n\n**Coverage**\n\nAll 13 entry points are answered, plus the IMD conservation invariant and the manifest. Six hold, seven carry a finding reference, none are unreached. Fork runs against live Robinhood Chain state were not executed here because there is no network. They remain owed by the launch's own verification.","treeHash":null,"usage":{"cachedInputTokens":1384453,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":29764,"runtime":"claude","turns":23,"wallClockMs":448122}}],"verification":[{"checks":[{"durationMs":3249,"exitCode":0,"name":"build","output":"Compiling 66 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.17s\nCompiler run successful!\n","passed":true},{"durationMs":181,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/Fork.t.sol:ForkTest\n[SKIP] test_fork_bindLaunchRejectsALaunchTheVaultDidNotRequest() (gas: 0)\n[SKIP] test_fork_distributeAsksTheRealOracleAndOpensAJob() (gas: 0)\n[SKIP] test_fork_launchPaysTheRealIntakeFromTheVault() (gas: 0)\n[SKIP] test_fork_theLiveContractsAreWhatTheVaultExpects() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 4 skipped; finished in 964.26µs (439.90µs CPU time)\n\nRan 5 tests for test/OracleConsumerConformance.t.sol:OracleConsumerConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 86674)\n[PASS] test_acceptsTheProtocolSignature() (gas: 123420)\n[PASS] test_callbackSelectorIsCanonical() (gas: 3626)\n[PASS] test_digestMatchesTheProtocol() (gas: 13677)\n[PASS] test_theCallbackTakesOnlyItsOwnRequests() (gas: 39983)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 946.66µs (1.16ms CPU time)\n\nRan 15 tests for test/JobsVault.t.sol:JobsVaultTest\n[PASS] test_aFlashBorrowedBalanceProvesNothing() (gas: 156000)\n[PASS] test_aHolderOpensAJobPaidFromTheVaultsIMD() (gas: 510847)\n[PASS] test_aNewRootReplacesTheSnapshot() (gas: 3248749)\n[PASS] test_aProofForAnotherHoldersLeafIsRefused() (gas: 149071)\n[PASS] test_aProvenHolderThatSoldIsRefused() (gas: 524357)\n[PASS] test_aSnapshotHolderProvesItsLeafOnce() (gas: 135126)\n[PASS] test_aSnapshotHoldingBelowOneMillionTokensIsRefused() (gas: 161972)\n[PASS] test_anEmptyBodyAndAnUnsoldActionAreRefused() (gas: 187564)\n[PASS] test_noJobsBeforeTheFirstRootArrives() (gas: 2873795)\n[PASS] test_noJobsBeforeTheTokenIsBound() (gas: 81253)\n[PASS] test_oneHoldingOpensOneJobHoweverManyWalletsItPassesThrough() (gas: 1702969)\n[PASS] test_oneJobPerWalletPerDay() (gas: 868966)\n[PASS] test_runsDryWhenTheIMDIsSpent() (gas: 15405991)\n[PASS] test_thereIsNoWithdraw() (gas: 78275)\n[PASS] test_twoSnapshotHoldersHaveTheirOwnClocks() (gas: 3085134)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 8.33ms (12.69ms CPU time)\n\nRan 25 tests for test/HolderRewards.t.sol:HolderRewardsTest\n[PASS] test_aBadOrForeignSignatureIsRefused() (gas: 230133)\n[PASS] test_aClaimNeedsAProofForTheCallersOwnLeaf() (gas: 277743)\n[PASS] test_aClosedEpochTakesNoClaimsAndNoSecondRollover() (gas: 2384244)\n[PASS] test_aDepositBelowThePriceIsHeldForTheNextEpoch() (gas: 2302064)\n[PASS] test_aDepositOpensAnEpochAndAsksTheOracleFromItsOwnIMD() (gas: 943999)\n[PASS] test_aDoubleClaimReverts() (gas: 358979)\n[PASS] test_aRefusedAskHoldsTheDepositWithoutAnEpoch() (gas: 2986527)\n[PASS] test_aRolledOverEpochWithoutARootMovesItsWholePot() (gas: 2503828)\n[PASS] test_aWeakPanelIsRefused() (gas: 176836)\n[PASS] test_aWrongAnswerTypeIsRefused() (gas: 116723)\n[PASS] test_aZeroBalanceLeafPaysNothing() (gas: 357360)\n[PASS] test_anAnswerIsStoredWithinTheStipedAndOnlyOnce() (gas: 337392)\n[PASS] test_anAnswerToAnotherQuestionIsRefused() (gas: 75561)\n[PASS] test_anExpiredAttestationIsRefused() (gas: 79822)\n[PASS] test_anOversizedClaimReverts() (gas: 368385)\n[PASS] test_fourHoldersClaimTheirShares() (gas: 630502)\n[PASS] test_headFallsBackToBlockNumberWithoutArbSys() (gas: 14780)\n[PASS] test_holdersClaimProRataOnce() (gas: 497206)\n[PASS] test_noClaimsBeforeTheRootOrOnAnUnknownEpoch() (gas: 297276)\n[PASS] test_onlyTheIntakeDeliversAnAnswer() (gas: 159200)\n[PASS] test_onlyTheVaultDeposits() (gas: 136184)\n[PASS] test_rolloverMovesExactlyPotMinusClaimedIntoTheCurrentEpoch() (gas: 2606087)\n[PASS] test_theCurrentEpochCannotRollOverIntoItself() (gas: 60872)\n[PASS] test_theLatestRootedEpochOnlyMovesForward() (gas: 2477611)\n[PASS] test_theQuestionFitsTheOraclesLimitsAndNamesEveryExclusion() (gas: 565753)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 8.33ms (26.03ms CPU time)\n\nRan 25 tests for test/SIMDVault.t.sol:SIMDVaultTest\n[PASS] testFuzz_splitConservesIMD(uint128,uint128) (runs: 256, μ: 2456756, ~: 3102545)\n[PASS] test_aFailedClaimNeverBlocksTheSplit() (gas: 2855674)\n[PASS] test_aFailedOracleAskNeverBlocksTheSplitTheBurnOrTheNativePayout() (gas: 3844858)\n[PASS] test_aMissingFactoryNeverBlocksTheSplit() (gas: 2775220)\n[PASS] test_aNativePayoutGoesToMarketingWithoutBlockingTheSplitOrTheBurn() (gas: 3097023)\n[PASS] test_aRevertingPriceReadNeverBlocksTheSplit() (gas: 902661)\n[PASS] test_aWalletThatRefusesEthDoesNotBlockTheSplit() (gas: 1040453679)\n[PASS] test_bindLaunchOnlyByTheDevWallet() (gas: 34944)\n[PASS] test_bindLaunchOnlyOnce() (gas: 200035)\n[PASS] test_bindLaunchRejectsALaunchWhoseRequesterIsNotTheVault() (gas: 196394)\n[PASS] test_bindLaunchRejectsATokenOutsideThePoolAndTheZeroToken() (gas: 76974)\n[PASS] test_bindLaunchRejectsIMDAsTheLaunchedToken() (gas: 61808)\n[PASS] test_bindLaunchStoresTheLaunchAndPassesTheTokenOn() (gas: 735567)\n[PASS] test_deploysItsChildrenAndHasNoOwner() (gas: 102099)\n[PASS] test_distributeAtMostOnceEverySevenDays() (gas: 5019856)\n[PASS] test_distributeClaimsSplitsAndBurns() (gas: 3145333)\n[PASS] test_distributeLeavesRoundingDustForNextWeek() (gas: 948259)\n[PASS] test_distributeOnlyAfterBindLaunch() (gas: 518124)\n[PASS] test_launchFollowsAMovedPrice() (gas: 534495)\n[PASS] test_launchOnlyByTheDevWallet() (gas: 43950)\n[PASS] test_launchOnlyOnce() (gas: 523848)\n[PASS] test_launchPaysExactPriceFromTheVaultAndMakesItThePayer() (gas: 595084)\n[PASS] test_launchRefusesAnEmptyBodyAndAnUnsoldAction() (gas: 126420)\n[PASS] test_launchRevertsWithoutEnoughIMD() (gas: 94096)\n[PASS] test_onlyTheVaultSetsTheChildrenTokens() (gas: 280822)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 79.88ms (100.91ms CPU time)\n\nRan 5 test suites in 80.62ms (98.44ms CPU time): 70 tests passed, 0 failed, 4 skipped (74 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HolderRewards.claim(uint256,uint256,bytes32[])\",\"HolderRewards.deposit(uint256)\",\"HolderRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"HolderRewards.rollover(uint256)\",\"HolderRewards.setToken(address,address)\",\"JobsVault.proveAndRequestJob(uint256,uint256,bytes32[],bytes)\",\"JobsVault.proveHolding(uint256,uint256,bytes32[])\",\"JobsVault.requestJob(bytes)\",\"JobsVault.setToken(address)\",\"SIMDVault.bindLaunch(uint64,address)\",\"SIMDVault.distribute()\",\"SIMDVault.launch(bytes)\",\"SIMDVault.receive()\"],\"files\":{\".gitignore\":5,\"README.md\":172,\"foundry.toml\":15,\"remappings.txt\":2,\"src/Fixed.sol\":32,\"src/HolderRewards.sol\":322,\"src/JobsVault.sol\":156,\"src/OracleAttestation.sol\":221,\"src/OracleQuestion.sol\":255,\"src/SIMDVault.sol\":157,\"src/interfaces/IArbSys.sol\":8,\"src/interfaces/IIntake.sol\":20,\"src/interfaces/ILaunchFactory.sol\":23,\"test/Base.t.sol\":166,\"test/Fork.t.sol\":195,\"test/HolderRewards.t.sol\":441,\"test/JobsVault.t.sol\":304,\"test/OracleConsumerConformance.t.sol\":127,\"test/SIMDVault.t.sol\":344,\"test/mocks/MockArbSys.sol\":11,\"test/mocks/MockERC20.sol\":14,\"test/mocks/MockIntake.sol\":80,\"test/mocks/MockLaunchFactory.sol\":68,\"test/mocks/MockSignerRegistry.sol\":22},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2687,"exitCode":0,"name":"slither","output":"[medium/medium] unused-return at src/SIMDVault.sol:100: SIMDVault.bindLaunch(uint64,address) (src/SIMDVault.sol#100-114) ignores return value by (key,None,None,requester) = LAUNCH_FACTORY.positionOf(launchNumber_) (src/SIMDVault.sol#105)\n[low/high] shadowing-local at src/HolderRewards.sol:109: HolderRewards.constructor(address).oracleSigner (src/HolderRewards.sol#109) shadows:\n[low/medium] missing-zero-check at src/HolderRewards.sol:116: HolderRewards.setToken(address,address).jobsVault_ (src/HolderRewards.sol#116) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/SIMDVault.sol:83: Reentrancy in SIMDVault.launch(bytes) (src/SIMDVault.sol#83-95):\n[low/medium] reentrancy-benign at src/HolderRewards.sol:140: Reentrancy in HolderRewards._openEpoch(uint256,uint256) (src/HolderRewards.sol#140-167):\n[low/medium] reentrancy-events at src/JobsVault.sol:117: Reentrancy in JobsVault._requestJob(bytes) (src/JobsVault.sol#117-135):\n[low/medium] reentrancy-events at src/HolderRewards.sol:140: Reentrancy in HolderRewards._openEpoch(uint256,uint256) (src/HolderRewards.sol#140-167):\n[low/medium] reentrancy-events at src/SIMDVault.sol:118: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#118-151):\n[low/medium] reentrancy-events at src/HolderRewards.sol:129: Reentrancy in HolderRewards.deposit(uint256) (src/HolderRewards.sol#129-136):\n[low/medium] reentrancy-events at src/SIMDVault.sol:100: Reentrancy in SIMDVault.bindLaunch(uint64,address) (src/SIMDVault.sol#100-114):\n[low/medium] reentrancy-events at src/SIMDVault.sol:118: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#118-151):\n[low/medium] reentrancy-events at src/SIMDVault.sol:118: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#118-151):\n[low/medium] reentrancy-events at src/SIMDVault.sol:83: Reentrancy in SIMDVault.launch(bytes) (src/SIMDVault.sol#83-95):\n[low/medium] timestamp at src/JobsVault.sol:139: JobsVault.canRequest(address) (src/JobsVault.sol#139-145) uses timestamp for comparisons\n[low/medium] timestamp at src/SIMDVault.sol:118: SIMDVault.distribute() (src/SIMDVault.sol#118-151) uses timestamp for comparisons\n[low/medium] timestamp at src/HolderRewards.sol:227: HolderRewards.rollover(uint256) (src/HolderRewards.sol#227-238) uses timestamp for comparisons\n[low/medium] timestamp at src/JobsVault.sol:117: JobsVault._requestJob(bytes) (src/JobsVault.sol#117-135) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAttestation.sol:157: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/OracleAttestation.sol#157-165) uses timestamp for comparisons","passed":true},{"durationMs":595,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/SIMDVault.sol:118: ETH transferred without address checks\n[high] reentrancy-state-change at src/JobsVault.sol:89: Reentrancy: State change after external call (5 places)\n[low] internal-function-used-once at src/OracleAttestation.sol:195: Internal Function Used Only Once (2 places)\n[low] large-numeric-literal at src/JobsVault.sol:53: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/OracleQuestion.sol:117: Literal Instead of Constant (13 places)\n[low] local-variable-shadowing at src/HolderRewards.sol:109: Local Variable Shadows State Variable\n[low] require-revert-in-loop at src/OracleQuestion.sol:114: Loop Contains `require`/`revert` (2 places)\n[low] state-no-address-check at src/HolderRewards.sol:121: Address State Variable Set Without Checks\n[low] uninitialized-local-variable at src/OracleQuestion.sol:114: Uninitialized Local Variable (4 places)","passed":true},{"durationMs":1820,"exitCode":0,"name":"proof e2fffde7a9ea","output":"Compiling 61 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.20s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-ef25bae2/Proof_e2fffde7a9ea.t.sol:JobsRecyclingProof\n[PASS] test_oneHoldingCannotOpenAJobFromEveryWalletItPassesThrough() (gas: 2039847)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.42ms (1.58ms CPU time)\n\nRan 1 test suite in 4.17ms (2.42ms 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":"236906d034f736ba79732dd39e72f3431b7fc495ae3658ca691f7d2382c6e615","verifiedTreeHash":"b89a6041f704ca796d769014fe6016c8b905c1ca","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":4512,"exitCode":0,"name":"build","output":"Compiling 69 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.42s\nCompiler run successful!\n","passed":true},{"durationMs":2526,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/Fork.t.sol:ForkTest\n[SKIP] test_fork_bindLaunchRejectsALaunchTheVaultDidNotRequest() (gas: 0)\n[SKIP] test_fork_distributeAsksTheRealOracleAndOpensAJob() (gas: 0)\n[SKIP] test_fork_launchPaysTheRealIntakeFromTheVault() (gas: 0)\n[SKIP] test_fork_theLiveContractsAreWhatTheVaultExpects() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 4 skipped; finished in 744.60µs (333.54µs CPU time)\n\nRan 5 tests for test/ForkLimits.t.sol:ForkLimitsTest\n[SKIP] test_fork_bindLaunchRejectsAnotherRequester() (gas: 0)\n[SKIP] test_fork_launchAndBindOnlyByTheDevWalletAndOnlyOnce() (gas: 0)\n[SKIP] test_fork_onlyTheLiveIntakeDeliversAndJobsAreLimited() (gas: 0)\n[SKIP] test_fork_sevenDayLimitWithTheLiveFactory() (gas: 0)\n[SKIP] test_fork_splitBurnAndNativeEthWithTheLiveFactoryAndIntake() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 5 skipped; finished in 797.06µs (173.55µs CPU time)\n\nRan 25 tests for test/SIMDVault.t.sol:SIMDVaultTest\n[PASS] testFuzz_splitConservesIMD(uint128,uint128) (runs: 256, μ: 2433926, ~: 3102551)\n[PASS] test_aFailedClaimNeverBlocksTheSplit() (gas: 2855674)\n[PASS] test_aFailedOracleAskNeverBlocksTheSplitTheBurnOrTheNativePayout() (gas: 3844858)\n[PASS] test_aMissingFactoryNeverBlocksTheSplit() (gas: 2775220)\n[PASS] test_aNativePayoutGoesToMarketingWithoutBlockingTheSplitOrTheBurn() (gas: 3097023)\n[PASS] test_aRevertingPriceReadNeverBlocksTheSplit() (gas: 902661)\n[PASS] test_aWalletThatRefusesEthDoesNotBlockTheSplit() (gas: 1040453679)\n[PASS] test_bindLaunchOnlyByTheDevWallet() (gas: 34944)\n[PASS] test_bindLaunchOnlyOnce() (gas: 200035)\n[PASS] test_bindLaunchRejectsALaunchWhoseRequesterIsNotTheVault() (gas: 196394)\n[PASS] test_bindLaunchRejectsATokenOutsideThePoolAndTheZeroToken() (gas: 76974)\n[PASS] test_bindLaunchRejectsIMDAsTheLaunchedToken() (gas: 61808)\n[PASS] test_bindLaunchStoresTheLaunchAndPassesTheTokenOn() (gas: 735567)\n[PASS] test_deploysItsChildrenAndHasNoOwner() (gas: 102099)\n[PASS] test_distributeAtMostOnceEverySevenDays() (gas: 5019856)\n[PASS] test_distributeClaimsSplitsAndBurns() (gas: 3145333)\n[PASS] test_distributeLeavesRoundingDustForNextWeek() (gas: 948259)\n[PASS] test_distributeOnlyAfterBindLaunch() (gas: 518124)\n[PASS] test_launchFollowsAMovedPrice() (gas: 534495)\n[PASS] test_launchOnlyByTheDevWallet() (gas: 43950)\n[PASS] test_launchOnlyOnce() (gas: 523848)\n[PASS] test_launchPaysExactPriceFromTheVaultAndMakesItThePayer() (gas: 595084)\n[PASS] test_launchRefusesAnEmptyBodyAndAnUnsoldAction() (gas: 126420)\n[PASS] test_launchRevertsWithoutEnoughIMD() (gas: 94096)\n[PASS] test_onlyTheVaultSetsTheChildrenTokens() (gas: 280822)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 193.53ms (217.05ms CPU time)\n\nRan 15 tests for test/JobsVault.t.sol:JobsVaultTest\n[PASS] test_aFlashBorrowedBalanceProvesNothing() (gas: 156000)\n[PASS] test_aHolderOpensAJobPaidFromTheVaultsIMD() (gas: 510847)\n[PASS] test_aNewRootReplacesTheSnapshot() (gas: 3248749)\n[PASS] test_aProofForAnotherHoldersLeafIsRefused() (gas: 149071)\n[PASS] test_aProvenHolderThatSoldIsRefused() (gas: 524357)\n[PASS] test_aSnapshotHolderProvesItsLeafOnce() (gas: 135126)\n[PASS] test_aSnapshotHoldingBelowOneMillionTokensIsRefused() (gas: 161972)\n[PASS] test_anEmptyBodyAndAnUnsoldActionAreRefused() (gas: 187564)\n[PASS] test_noJobsBeforeTheFirstRootArrives() (gas: 2873795)\n[PASS] test_noJobsBeforeTheTokenIsBound() (gas: 81253)\n[PASS] test_oneHoldingOpensOneJobHoweverManyWalletsItPassesThrough() (gas: 1702969)\n[PASS] test_oneJobPerWalletPerDay() (gas: 868966)\n[PASS] test_runsDryWhenTheIMDIsSpent() (gas: 15405991)\n[PASS] test_thereIsNoWithdraw() (gas: 78275)\n[PASS] test_twoSnapshotHoldersHaveTheirOwnClocks() (gas: 3085134)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 207.39ms (14.06ms CPU time)\n\nRan 5 tests for test/OracleConsumerConformance.t.sol:OracleConsumerConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 86674)\n[PASS] test_acceptsTheProtocolSignature() (gas: 123420)\n[PASS] test_callbackSelectorIsCanonical() (gas: 3626)\n[PASS] test_digestMatchesTheProtocol() (gas: 13677)\n[PASS] test_theCallbackTakesOnlyItsOwnRequests() (gas: 39983)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 207.31ms (1.43ms CPU time)\n\nRan 25 tests for test/HolderRewards.t.sol:HolderRewardsTest\n[PASS] test_aBadOrForeignSignatureIsRefused() (gas: 230133)\n[PASS] test_aClaimNeedsAProofForTheCallersOwnLeaf() (gas: 277743)\n[PASS] test_aClosedEpochTakesNoClaimsAndNoSecondRollover() (gas: 2384244)\n[PASS] test_aDepositBelowThePriceIsHeldForTheNextEpoch() (gas: 2302064)\n[PASS] test_aDepositOpensAnEpochAndAsksTheOracleFromItsOwnIMD() (gas: 943999)\n[PASS] test_aDoubleClaimReverts() (gas: 358979)\n[PASS] test_aRefusedAskHoldsTheDepositWithoutAnEpoch() (gas: 2986527)\n[PASS] test_aRolledOverEpochWithoutARootMovesItsWholePot() (gas: 2503828)\n[PASS] test_aWeakPanelIsRefused() (gas: 176836)\n[PASS] test_aWrongAnswerTypeIsRefused() (gas: 116723)\n[PASS] test_aZeroBalanceLeafPaysNothing() (gas: 357360)\n[PASS] test_anAnswerIsStoredWithinTheStipedAndOnlyOnce() (gas: 337392)\n[PASS] test_anAnswerToAnotherQuestionIsRefused() (gas: 75561)\n[PASS] test_anExpiredAttestationIsRefused() (gas: 79822)\n[PASS] test_anOversizedClaimReverts() (gas: 368385)\n[PASS] test_fourHoldersClaimTheirShares() (gas: 630502)\n[PASS] test_headFallsBackToBlockNumberWithoutArbSys() (gas: 14780)\n[PASS] test_holdersClaimProRataOnce() (gas: 497206)\n[PASS] test_noClaimsBeforeTheRootOrOnAnUnknownEpoch() (gas: 297276)\n[PASS] test_onlyTheIntakeDeliversAnAnswer() (gas: 159200)\n[PASS] test_onlyTheVaultDeposits() (gas: 136184)\n[PASS] test_rolloverMovesExactlyPotMinusClaimedIntoTheCurrentEpoch() (gas: 2606087)\n[PASS] test_theCurrentEpochCannotRollOverIntoItself() (gas: 60872)\n[PASS] test_theLatestRootedEpochOnlyMovesForward() (gas: 2477611)\n[PASS] test_theQuestionFitsTheOraclesLimitsAndNamesEveryExclusion() (gas: 565753)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 212.64ms (31.99ms CPU time)\n\nRan 14 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_theSplitIsExactForAnyBalance(uint256) (runs: 1000, μ: 2014789, ~: 1062201)\nLogs:\n  Bound result 6109500045376071123126527987131703875950\n\n[PASS] testFuzz_twoHoldersNeverTakeMoreThanThePot(uint256,uint256,uint256) (runs: 1000, μ: 3309804, ~: 3329684)\nLogs:\n  Bound result 3871\n  Bound result 859391852546991089431068138315785986\n  Bound result 10012\n\n[PASS] test_aLateAnswerForARolledOverEpochPaysNothing() (gas: 5251674)\n[PASS] test_aProofForOneEpochDoesNotPayInAnother() (gas: 5512295)\n[PASS] test_aRevertingOracleAskNoLongerFreezesDistribute() (gas: 3845027)\n[PASS] test_aTokenWithoutDecimalsIsTreatedAsEighteen() (gas: 339568)\n[PASS] test_anEmptyWeekDistributesNothingAndStillWaitsSevenDays() (gas: 942299)\n[PASS] test_distributeBeforeBindRevertsEvenWithFundsAndFees() (gas: 646629)\n[PASS] test_epochOnesAnswerIsRefusedForEpochTwosRequest() (gas: 5075942)\n[PASS] test_imdCannotBeBoundAsTheLaunchedToken() (gas: 270709)\n[PASS] test_jobsWithoutIMDRevertAndDoNotStartTheClock() (gas: 3355779)\n[PASS] test_oneHoldingPassedThroughTenWalletsOpensOneJob() (gas: 4530886)\n[PASS] test_rolloverOneSecondEarlyRevertsAndAnUnknownEpochIsRefused() (gas: 5037792)\n[PASS] test_theHoldingThresholdIsOneMillionWholeTokensAtSixDecimals() (gas: 3330741)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 212.93ms (436.90ms CPU time)\n\nRan 2 tests for test/invariant/SystemInvariant.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariant_everyActionBehavedAsTheBriefSays\n[PASS] invariant_imdOnlyMovesAlongTheBriefsPaths\n[PASS] invariant_noEpochPaysBeyondItsPotAndReservedIsHeld\n[PASS] invariant_tokensAreBurnedAndEthGoesToMarketing\n SystemInvariantTest invariants (runs: 256, calls: 10240, reverts: 0)\n\n╭---------------+-------------------+-------+---------+----------╮\n| Contract      | Selector          | Calls | Reverts | Discards |\n+================================================================+\n| SystemHandler | answer            | 824   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | claim             | 829   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | distribute        | 1735  | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | donateImd         | 878   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | outsiderPokes     | 890   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | requestJob        | 848   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | rollover          | 878   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | sendEth           | 815   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | toggleOraclePrice | 853   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | transferTokens    | 786   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | warp              | 904   | 0       | 0        |\n╰---------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1999999999999500000000001\n  Bound result 144760\n  Bound result 32\n  Bound result 6\n  Bound result 2366482869645213223\n  Bound result 887200\n  Bound result 65\n  Bound result 20324\n  Bound result 52715\n  Bound result 887200\n  Bound result 10000000000000000000\n  Bound result 1\n  Bound result 864000\n  Bound result 221523\n  Bound result 948\n  Bound result 102\n  Bound result 9557\n  Bound result 0\n  Bound result 4872\n  Bound result 810915\n  Bound result 66\n  Bound result 10\n  Bound result 7\n  Bound result 48\n  Bound result 352000\n  Bound result 6659\n  Bound result 305\n  Bound result 2\n  Bound result 512\n  Bound result 1000000\n  Bound result 86397\n  Bound result 604800\n  Bound result 392450\n  Bound result 998999999999999999001\n  Bound result 9347\n  Bound result 1590109535755964701\n  Bound result 1613155\n  Bound result 2448\n  Bound result 2664\n  Bound result 12\n\n[PASS] test_theHandlerReachesEveryPath() (gas: 7553944)\nLogs:\n  Bound result 0\n  Bound result 10000000000000000000\n  Bound result 1000000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1\n  Bound result 1000000000000000000000000\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 1\n  Bound result 1\n  Bound result 0\n  Bound result 0\n  Bound result 3600\n  Bound result 0\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 604800\n  Bound result 5000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 2\n  Bound result 1000000000000000000000000\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 2\n  Bound result 2\n  Bound result 2419200\n  Bound result 1\n  Bound result 1\n  Bound result 86400\n  Bound result 0\n  Bound result 0\n  Bound result 0\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.43s (2.43s CPU time)\n\nRan 8 test suites in 2.43s (3.46s CPU time): 86 tests passed, 0 failed, 9 skipped (95 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HolderRewards.claim(uint256,uint256,bytes32[])\",\"HolderRewards.deposit(uint256)\",\"HolderRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"HolderRewards.rollover(uint256)\",\"HolderRewards.setToken(address,address)\",\"JobsVault.proveAndRequestJob(uint256,uint256,bytes32[],bytes)\",\"JobsVault.proveHolding(uint256,uint256,bytes32[])\",\"JobsVault.requestJob(bytes)\",\"JobsVault.setToken(address)\",\"SIMDVault.bindLaunch(uint64,address)\",\"SIMDVault.distribute()\",\"SIMDVault.launch(bytes)\",\"SIMDVault.receive()\"],\"files\":{\".gitignore\":5,\"README.md\":172,\"foundry.toml\":15,\"launch.json\":10,\"remappings.txt\":2,\"src/Fixed.sol\":32,\"src/HolderRewards.sol\":322,\"src/JobsVault.sol\":156,\"src/OracleAttestation.sol\":221,\"src/OracleQuestion.sol\":255,\"src/SIMDVault.sol\":157,\"src/interfaces/IArbSys.sol\":8,\"src/interfaces/IIntake.sol\":20,\"src/interfaces/ILaunchFactory.sol\":23,\"test/Base.t.sol\":166,\"test/EdgeCases.t.sol\":314,\"test/Fork.t.sol\":195,\"test/ForkLimits.t.sol\":210,\"test/HolderRewards.t.sol\":441,\"test/JobsVault.t.sol\":304,\"test/OracleConsumerConformance.t.sol\":127,\"test/SIMDVault.t.sol\":344,\"test/invariant/SystemInvariant.t.sol\":495,\"test/mocks/MockArbSys.sol\":11,\"test/mocks/MockERC20.sol\":14,\"test/mocks/MockIntake.sol\":80,\"test/mocks/MockLaunchFactory.sol\":68,\"test/mocks/MockSignerRegistry.sol\":22},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"283c30d79d3985329be180c1715a6ca9cd227cc79f7eea27ead3cf21125a469a","verifiedTreeHash":"59c594941f6881a99d27afb8ca5c4717c9937a63","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":5100,"exitCode":0,"name":"build","output":"Compiling 69 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.01s\nCompiler run successful!\n","passed":true},{"durationMs":2469,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/ForkLimits.t.sol:ForkLimitsTest\n[SKIP] test_fork_bindLaunchRejectsAnotherRequester() (gas: 0)\n[SKIP] test_fork_launchAndBindOnlyByTheDevWalletAndOnlyOnce() (gas: 0)\n[SKIP] test_fork_onlyTheLiveIntakeDeliversAndJobsAreLimited() (gas: 0)\n[SKIP] test_fork_sevenDayLimitWithTheLiveFactory() (gas: 0)\n[SKIP] test_fork_splitBurnAndNativeEthWithTheLiveFactoryAndIntake() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 5 skipped; finished in 795.29µs (276.48µs CPU time)\n\nRan 4 tests for test/Fork.t.sol:ForkTest\n[SKIP] test_fork_bindLaunchRejectsALaunchTheVaultDidNotRequest() (gas: 0)\n[SKIP] test_fork_distributeAsksTheRealOracleAndOpensAJob() (gas: 0)\n[SKIP] test_fork_launchPaysTheRealIntakeFromTheVault() (gas: 0)\n[SKIP] test_fork_theLiveContractsAreWhatTheVaultExpects() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 4 skipped; finished in 9.01ms (452.88µs CPU time)\n\nRan 5 tests for test/OracleConsumerConformance.t.sol:OracleConsumerConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 86695)\n[PASS] test_acceptsTheProtocolSignature() (gas: 123530)\n[PASS] test_callbackSelectorIsCanonical() (gas: 3626)\n[PASS] test_digestMatchesTheProtocol() (gas: 13610)\n[PASS] test_theCallbackTakesOnlyItsOwnRequests() (gas: 39949)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 103.43ms (17.31ms CPU time)\n\nRan 27 tests for test/SIMDVault.t.sol:SIMDVaultTest\n[PASS] testFuzz_splitConservesIMD(uint128,uint128) (runs: 256, μ: 2445858, ~: 3132061)\n[PASS] test_aFailedClaimNeverBlocksTheSplit() (gas: 2885253)\n[PASS] test_aFailedClaimWithNothingToSplitDoesNotSpendTheWeek() (gas: 3412354)\n[PASS] test_aFailedOracleAskNeverBlocksTheSplitTheBurnOrTheNativePayout() (gas: 3878944)\n[PASS] test_aMissingFactoryNeverBlocksTheSplit() (gas: 2804791)\n[PASS] test_aNativePayoutGoesToMarketingWithoutBlockingTheSplitOrTheBurn() (gas: 3126696)\n[PASS] test_aRevertingPriceReadNeverBlocksTheSplit() (gas: 925195)\n[PASS] test_aWalletThatRefusesEthDoesNotBlockTheSplit() (gas: 1040454147)\n[PASS] test_bindLaunchOnlyByTheDevWallet() (gas: 34922)\n[PASS] test_bindLaunchOnlyOnce() (gas: 200256)\n[PASS] test_bindLaunchRejectsALaunchWhoseRequesterIsNotTheVault() (gas: 196487)\n[PASS] test_bindLaunchRejectsAPoolNotPairedWithIMD() (gas: 134576)\n[PASS] test_bindLaunchRejectsATokenOutsideThePoolAndTheZeroToken() (gas: 76974)\n[PASS] test_bindLaunchRejectsIMDAsTheLaunchedToken() (gas: 61885)\n[PASS] test_bindLaunchStoresTheLaunchAndPassesTheTokenOn() (gas: 735854)\n[PASS] test_deploysItsChildrenAndHasNoOwner() (gas: 102208)\n[PASS] test_distributeAtMostOnceEverySevenDays() (gas: 5058597)\n[PASS] test_distributeClaimsSplitsAndBurns() (gas: 3177264)\n[PASS] test_distributeLeavesRoundingDustForNextWeek() (gas: 970953)\n[PASS] test_distributeOnlyAfterBindLaunch() (gas: 518146)\n[PASS] test_launchFollowsAMovedPrice() (gas: 534495)\n[PASS] test_launchOnlyByTheDevWallet() (gas: 43950)\n[PASS] test_launchOnlyOnce() (gas: 523848)\n[PASS] test_launchPaysExactPriceFromTheVaultAndMakesItThePayer() (gas: 595084)\n[PASS] test_launchRefusesAnEmptyBodyAndAnUnsoldAction() (gas: 126420)\n[PASS] test_launchRevertsWithoutEnoughIMD() (gas: 94096)\n[PASS] test_onlyTheVaultSetsTheChildrenTokens() (gas: 281263)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 206.11ms (150.15ms CPU time)\n\nRan 18 tests for test/JobsVault.t.sol:JobsVaultTest\n[PASS] test_aDormantOracleLetsLiveHoldersOpenJobsWithoutAProof() (gas: 3162341)\n[PASS] test_aFlashBorrowedBalanceProvesNothing() (gas: 161577)\n[PASS] test_aHolderOpensAJobPaidFromTheVaultsIMD() (gas: 513079)\n[PASS] test_aNewRootReplacesTheSnapshot() (gas: 3316316)\n[PASS] test_aProofForAnotherHoldersLeafIsRefused() (gas: 149099)\n[PASS] test_aProvenHolderThatSoldIsRefused() (gas: 526481)\n[PASS] test_aSnapshotHolderProvesItsLeafOnce() (gas: 135166)\n[PASS] test_aSnapshotHoldingBelowOneMillionTokensIsRefused() (gas: 164753)\n[PASS] test_aTokenWithTooManyDecimalsBindsWithTheEighteenDecimalMinimum() (gas: 815667)\n[PASS] test_anEmptyBodyAndAnUnsoldActionAreRefused() (gas: 187627)\n[PASS] test_anUnsoldJobActionIsRecoverableSixtyDaysAfterItIsNoticed() (gas: 908104)\n[PASS] test_noJobsBeforeTheFirstRootArrives() (gas: 2912868)\n[PASS] test_noJobsBeforeTheTokenIsBound() (gas: 81333)\n[PASS] test_oneHoldingOpensOneJobHoweverManyWalletsItPassesThrough() (gas: 1730054)\n[PASS] test_oneJobPerWalletPerDay() (gas: 873126)\n[PASS] test_runsDryWhenTheIMDIsSpent() (gas: 15518488)\n[PASS] test_thereIsNoWithdraw() (gas: 78385)\n[PASS] test_twoSnapshotHoldersHaveTheirOwnClocks() (gas: 3149897)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 208.40ms (22.42ms CPU time)\n\nRan 31 tests for test/HolderRewards.t.sol:HolderRewardsTest\n[PASS] test_aBadOrForeignSignatureIsRefused() (gas: 232123)\n[PASS] test_aClaimNeedsAProofForTheCallersOwnLeaf() (gas: 304422)\n[PASS] test_aClosedEpochTakesNoClaimsAndNoSecondRollover() (gas: 2447040)\n[PASS] test_aDepositBelowThePriceIsHeldForTheNextEpoch() (gas: 2315800)\n[PASS] test_aDepositOpensAnEpochAndAsksTheOracleFromItsOwnIMD() (gas: 946309)\n[PASS] test_aDoubleClaimReverts() (gas: 385615)\n[PASS] test_aPotThinnerThanTwiceTheOraclePriceIsHeld() (gas: 2334100)\n[PASS] test_aQuietOracleBecomesDormantSixtyDaysAfterAnUnansweredDeposit() (gas: 2467219)\n[PASS] test_aRefusedAskHoldsTheDepositWithoutAnEpoch() (gas: 3000229)\n[PASS] test_aRetiredOracleActionLeavesNothingStrandedForGood() (gas: 2050564)\n[PASS] test_aRolledOverEpochWithoutARootMovesItsWholePot() (gas: 2546273)\n[PASS] test_aWeakPanelIsRefused() (gas: 178870)\n[PASS] test_aWrongAnswerTypeIsRefused() (gas: 118960)\n[PASS] test_aZeroBalanceLeafPaysNothing() (gas: 384048)\n[PASS] test_anAnswerIsStoredWithinTheStipedAndOnlyOnce() (gas: 366175)\n[PASS] test_anAnswerToAnotherQuestionIsRefused() (gas: 77662)\n[PASS] test_anExpiredAttestationIsRefused() (gas: 81992)\n[PASS] test_anOversizedClaimReverts() (gas: 395071)\n[PASS] test_fourHoldersClaimTheirShares() (gas: 657269)\n[PASS] test_headFallsBackToBlockNumberWithoutArbSys() (gas: 14769)\n[PASS] test_holdersClaimProRataOnce() (gas: 526127)\n[PASS] test_noClaimsBeforeTheRootOrOnAnUnknownEpoch() (gas: 324047)\n[PASS] test_onlyTheIntakeDeliversAnAnswer() (gas: 163511)\n[PASS] test_onlyTheVaultDeposits() (gas: 136250)\n[PASS] test_recoverDormantNeverTouchesAnOpenEpochsPot() (gas: 2565594)\n[PASS] test_rolloverIntoAClosedCurrentEpochIsCarried() (gas: 2258311)\n[PASS] test_rolloverIntoARootedEpochPassesThroughSoEqualLeavesArePaidEqually() (gas: 5007417)\n[PASS] test_rolloverMovesExactlyPotMinusClaimedIntoARootlessCurrentEpoch() (gas: 2688267)\n[PASS] test_theCurrentEpochRollsOverIntoTheCarryAfterTheDelay() (gas: 2253961)\n[PASS] test_theLatestRootedEpochOnlyMovesForward() (gas: 2541903)\n[PASS] test_theQuestionFitsTheOraclesLimitsAndNamesEveryExclusion() (gas: 565863)\nSuite result: ok. 31 passed; 0 failed; 0 skipped; finished in 208.43ms (100.07ms CPU time)\n\nRan 17 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_theSplitIsExactForAnyBalance(uint256) (runs: 1000, μ: 2028332, ~: 1084918)\nLogs:\n  Bound result 1031785573717176236557909203166936\n\n[PASS] testFuzz_twoHoldersNeverTakeMoreThanThePot(uint256,uint256,uint256) (runs: 1000, μ: 3368254, ~: 3388528)\nLogs:\n  Bound result 533946884204775804814344135452161\n  Bound result 986081529981069612968957426867619987\n  Bound result 16797755450\n\n[PASS] test_aFailedClaimWithNoIMDKeepsTheWeekButStillBurns() (gas: 3335114)\n[PASS] test_aLateAnswerForARolledOverEpochPaysNothing() (gas: 5323990)\n[PASS] test_aProofForOneEpochDoesNotPayInAnother() (gas: 5611295)\n[PASS] test_aRevertingOracleAskNoLongerFreezesDistribute() (gas: 3879251)\n[PASS] test_aTokenWithoutDecimalsIsTreatedAsEighteen() (gas: 339792)\n[PASS] test_anEmptyWeekDistributesNothingAndStillWaitsSevenDays() (gas: 942516)\n[PASS] test_distributeBeforeBindRevertsEvenWithFundsAndFees() (gas: 646670)\n[PASS] test_epochOnesAnswerIsRefusedForEpochTwosRequest() (gas: 5123869)\n[PASS] test_imdCannotBeBoundAsTheLaunchedToken() (gas: 271038)\n[PASS] test_jobsRecoverDormantFailurePaths() (gas: 3862161)\n[PASS] test_jobsWithoutIMDRevertAndDoNotStartTheClock() (gas: 3414278)\n[PASS] test_oneHoldingPassedThroughTenWalletsOpensOneJob() (gas: 4614896)\n[PASS] test_rolloverOneSecondEarlyRevertsAndAnUnknownEpochIsRefused() (gas: 5080980)\n[PASS] test_theHoldingThresholdIsOneMillionWholeTokensAtSixDecimals() (gas: 3389207)\n[PASS] test_theReportedNoRootReproductionNowOpensAJobAfterSixtyDays() (gas: 12405734)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 211.07ms (495.92ms CPU time)\n\nRan 2 tests for test/invariant/SystemInvariant.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariant_everyActionBehavedAsTheBriefSays\n[PASS] invariant_imdOnlyMovesAlongTheBriefsPaths\n[PASS] invariant_noEpochPaysBeyondItsPotAndReservedIsHeld\n[PASS] invariant_tokensAreBurnedAndEthGoesToMarketing\n SystemInvariantTest invariants (runs: 256, calls: 10240, reverts: 0)\n\n╭---------------+----------------------+-------+---------+----------╮\n| Contract      | Selector             | Calls | Reverts | Discards |\n+===================================================================+\n| SystemHandler | answer               | 707   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | claim                | 692   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | distribute           | 1394  | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | donateImd            | 713   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | noticeAndRecoverJobs | 676   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | outsiderPokes        | 666   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | recoverRewards       | 709   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | requestJob           | 716   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | rollover             | 652   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | sendEth              | 713   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | toggleJobPrice       | 648   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | toggleOraclePrice    | 652   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | transferTokens       | 709   | 0       | 0        |\n|---------------+----------------------+-------+---------+----------|\n| SystemHandler | warp                 | 593   | 0       | 0        |\n╰---------------+----------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 19765\n  Bound result 736000\n  Bound result 0\n  Bound result 773089\n  Bound result 17101\n  Bound result 52715\n  Bound result 77239839554726401057\n  Bound result 79707264\n  Bound result 19296\n  Bound result 4000\n  Bound result 504000000000000000\n  Bound result 85583\n  Bound result 6702\n  Bound result 1\n  Bound result 1\n  Bound result 25528068721566395351130589\n  Bound result 778492195493342481409423495\n  Bound result 516031926784357414107950295\n  Bound result 446152737803370941365546179\n  Bound result 38368\n  Bound result 604800\n  Bound result 24542\n  Bound result 10000000000000000000000000\n  Bound result 1812\n  Bound result 7340\n  Bound result 10425\n  Bound result 15866\n  Bound result 4679571542821094549\n  Bound result 530713\n  Bound result 300\n  Bound result 92\n  Bound result 2114\n  Bound result 153647\n  Bound result 34\n  Bound result 527\n  Bound result 54880\n  Bound result 2750\n  Bound result 1\n  Bound result 0\n  Bound result 3024000\n  Bound result 11966\n  Bound result 172800\n  Bound result 2945\n  Bound result 71\n  Bound result 5719999999999999998\n  Bound result 76647\n\n[PASS] test_theHandlerReachesEveryPath() (gas: 7768303)\nLogs:\n  Bound result 0\n  Bound result 10000000000000000000\n  Bound result 1000000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1\n  Bound result 1000000000000000000000000\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 1\n  Bound result 1\n  Bound result 0\n  Bound result 0\n  Bound result 3600\n  Bound result 0\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 604800\n  Bound result 5000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 2\n  Bound result 1000000000000000000000000\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 2\n  Bound result 2\n  Bound result 2419200\n  Bound result 1\n  Bound result 1\n  Bound result 86400\n  Bound result 0\n  Bound result 0\n  Bound result 0\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.36s (2.36s CPU time)\n\nRan 8 test suites in 2.36s (3.31s CPU time): 100 tests passed, 0 failed, 9 skipped (109 total tests)\n","passed":true},{"durationMs":65,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HolderRewards.claim(uint256,uint256,bytes32[])\",\"HolderRewards.deposit(uint256)\",\"HolderRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"HolderRewards.recoverDormant()\",\"HolderRewards.rollover(uint256)\",\"HolderRewards.setToken(address,address)\",\"JobsVault.noticeUnsold()\",\"JobsVault.proveAndRequestJob(uint256,uint256,bytes32[],bytes)\",\"JobsVault.proveHolding(uint256,uint256,bytes32[])\",\"JobsVault.recoverDormant()\",\"JobsVault.requestJob(bytes)\",\"JobsVault.setToken(address)\",\"SIMDVault.bindLaunch(uint64,address)\",\"SIMDVault.distribute()\",\"SIMDVault.launch(bytes)\",\"SIMDVault.receive()\"],\"files\":{\".gitignore\":5,\"README.md\":224,\"foundry.toml\":15,\"remappings.txt\":2,\"src/Fixed.sol\":32,\"src/HolderRewards.sol\":398,\"src/JobsVault.sol\":217,\"src/OracleAttestation.sol\":221,\"src/OracleQuestion.sol\":255,\"src/SIMDVault.sol\":164,\"src/interfaces/IArbSys.sol\":8,\"src/interfaces/IIntake.sol\":20,\"src/interfaces/ILaunchFactory.sol\":23,\"test/Base.t.sol\":166,\"test/EdgeCases.t.sol\":434,\"test/Fork.t.sol\":195,\"test/ForkLimits.t.sol\":210,\"test/HolderRewards.t.sol\":600,\"test/JobsVault.t.sol\":403,\"test/OracleConsumerConformance.t.sol\":127,\"test/SIMDVault.t.sol\":380,\"test/invariant/SystemInvariant.t.sol\":571,\"test/mocks/MockArbSys.sol\":11,\"test/mocks/MockERC20.sol\":14,\"test/mocks/MockIntake.sol\":80,\"test/mocks/MockLaunchFactory.sol\":68,\"test/mocks/MockSignerRegistry.sol\":22},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"948a45d9275599eecc133bda5de3a7e2e1c0b368756c29aa820087ec41e441cf","verifiedTreeHash":"3b4724811a771a7e326fa6e63a5ee64850716f39","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":4109,"exitCode":0,"name":"build","output":"Compiling 69 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.02s\nCompiler run successful!\n","passed":true},{"durationMs":2494,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/ForkLimits.t.sol:ForkLimitsTest\n[SKIP] test_fork_bindLaunchRejectsAnotherRequester() (gas: 0)\n[SKIP] test_fork_launchAndBindOnlyByTheDevWalletAndOnlyOnce() (gas: 0)\n[SKIP] test_fork_onlyTheLiveIntakeDeliversAndJobsAreLimited() (gas: 0)\n[SKIP] test_fork_sevenDayLimitWithTheLiveFactory() (gas: 0)\n[SKIP] test_fork_splitBurnAndNativeEthWithTheLiveFactoryAndIntake() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 5 skipped; finished in 590.13µs (279.79µs CPU time)\n\nRan 4 tests for test/Fork.t.sol:ForkTest\n[SKIP] test_fork_bindLaunchRejectsALaunchTheVaultDidNotRequest() (gas: 0)\n[SKIP] test_fork_distributeAsksTheRealOracleAndOpensAJob() (gas: 0)\n[SKIP] test_fork_launchPaysTheRealIntakeFromTheVault() (gas: 0)\n[SKIP] test_fork_theLiveContractsAreWhatTheVaultExpects() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 4 skipped; finished in 581.24µs (201.03µs CPU time)\n\nRan 5 tests for test/OracleConsumerConformance.t.sol:OracleConsumerConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 86629)\n[PASS] test_acceptsTheProtocolSignature() (gas: 123442)\n[PASS] test_callbackSelectorIsCanonical() (gas: 3626)\n[PASS] test_digestMatchesTheProtocol() (gas: 13610)\n[PASS] test_theCallbackTakesOnlyItsOwnRequests() (gas: 40005)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 969.36µs (1.12ms CPU time)\n\nRan 8 tests for test/JobsVault.t.sol:JobsVaultTest\n[PASS] test_aHolderOpensAJobPaidFromTheVaultsIMD() (gas: 419237)\n[PASS] test_anEmptyBodyAndAnUnsoldActionAreRefused() (gas: 113682)\n[PASS] test_holdingBelowOneMillionTokensIsRefused() (gas: 391708)\n[PASS] test_movingTokensDoesNotResetTheClock() (gas: 760325)\n[PASS] test_noJobsBeforeTheTokenIsBound() (gas: 39631)\n[PASS] test_oneJobPerWalletPerDay() (gas: 1046440)\n[PASS] test_runsDryWhenTheIMDIsSpent() (gas: 18401462)\n[PASS] test_thereIsNoWithdraw() (gas: 78335)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 10.80ms (5.69ms CPU time)\n\nRan 23 tests for test/HolderRewards.t.sol:HolderRewardsTest\n[PASS] test_aBadOrForeignSignatureIsRefused() (gas: 230221)\n[PASS] test_aClaimNeedsAProofForTheCallersOwnLeaf() (gas: 255680)\n[PASS] test_aClosedEpochTakesNoClaimsAndNoSecondRollover() (gas: 2362073)\n[PASS] test_aDepositBelowThePriceIsHeldForTheNextEpoch() (gas: 2301996)\n[PASS] test_aDepositOpensAnEpochAndAsksTheOracleFromItsOwnIMD() (gas: 944065)\n[PASS] test_aDoubleClaimReverts() (gas: 336916)\n[PASS] test_aRolledOverEpochWithoutARootMovesItsWholePot() (gas: 2481701)\n[PASS] test_aWeakPanelIsRefused() (gas: 176924)\n[PASS] test_aWrongAnswerTypeIsRefused() (gas: 116767)\n[PASS] test_aZeroBalanceLeafPaysNothing() (gas: 335297)\n[PASS] test_anAnswerIsStoredWithinTheStipedAndOnlyOnce() (gas: 294632)\n[PASS] test_anAnswerToAnotherQuestionIsRefused() (gas: 75605)\n[PASS] test_anExpiredAttestationIsRefused() (gas: 79866)\n[PASS] test_anOversizedClaimReverts() (gas: 346322)\n[PASS] test_fourHoldersClaimTheirShares() (gas: 608483)\n[PASS] test_headFallsBackToBlockNumberWithoutArbSys() (gas: 14758)\n[PASS] test_holdersClaimProRataOnce() (gas: 475165)\n[PASS] test_noClaimsBeforeTheRootOrOnAnUnknownEpoch() (gas: 275257)\n[PASS] test_onlyTheIntakeDeliversAnAnswer() (gas: 159310)\n[PASS] test_onlyTheVaultDeposits() (gas: 136206)\n[PASS] test_rolloverMovesExactlyPotMinusClaimedIntoTheCurrentEpoch() (gas: 2584004)\n[PASS] test_theCurrentEpochCannotRollOverIntoItself() (gas: 60872)\n[PASS] test_theQuestionFitsTheOraclesLimitsAndNamesEveryExclusion() (gas: 565753)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 142.87ms (32.15ms CPU time)\n\nRan 22 tests for test/SIMDVault.t.sol:SIMDVaultTest\n[PASS] testFuzz_splitConservesIMD(uint128,uint128) (runs: 256, μ: 2365443, ~: 3102363)\n[PASS] test_aFailedClaimNeverBlocksTheSplit() (gas: 2855538)\n[PASS] test_aMissingFactoryNeverBlocksTheSplit() (gas: 2774998)\n[PASS] test_aNativePayoutGoesToMarketingWithoutBlockingTheSplitOrTheBurn() (gas: 3096790)\n[PASS] test_aWalletThatRefusesEthDoesNotBlockTheSplit() (gas: 1040453673)\n[PASS] test_bindLaunchOnlyByTheDevWallet() (gas: 34944)\n[PASS] test_bindLaunchOnlyOnce() (gas: 199986)\n[PASS] test_bindLaunchRejectsALaunchWhoseRequesterIsNotTheVault() (gas: 196306)\n[PASS] test_bindLaunchRejectsATokenOutsideThePoolAndTheZeroToken() (gas: 76952)\n[PASS] test_bindLaunchStoresTheLaunchAndPassesTheTokenOn() (gas: 735584)\n[PASS] test_deploysItsChildrenAndHasNoOwner() (gas: 93709)\n[PASS] test_distributeAtMostOnceEverySevenDays() (gas: 5019635)\n[PASS] test_distributeClaimsSplitsAndBurns() (gas: 3145242)\n[PASS] test_distributeLeavesRoundingDustForNextWeek() (gas: 948143)\n[PASS] test_distributeOnlyAfterBindLaunch() (gas: 518124)\n[PASS] test_launchFollowsAMovedPrice() (gas: 534406)\n[PASS] test_launchOnlyByTheDevWallet() (gas: 43994)\n[PASS] test_launchOnlyOnce() (gas: 523781)\n[PASS] test_launchPaysExactPriceFromTheVaultAndMakesItThePayer() (gas: 595084)\n[PASS] test_launchRefusesAnEmptyBodyAndAnUnsoldAction() (gas: 126464)\n[PASS] test_launchRevertsWithoutEnoughIMD() (gas: 94096)\n[PASS] test_onlyTheVaultSetsTheChildrenTokens() (gas: 280771)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 186.93ms (168.11ms CPU time)\n\nRan 11 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_theSplitIsExactForAnyBalance(uint256) (runs: 1000, μ: 2008851, ~: 1062125)\nLogs:\n  Bound result 6109500045376071123126527987131703875950\n\n[PASS] testFuzz_twoHoldersNeverTakeMoreThanThePot(uint256,uint256,uint256) (runs: 1000, μ: 3286823, ~: 3307574)\nLogs:\n  Bound result 20331\n  Bound result 11174\n  Bound result 2592000\n\n[PASS] test_aLateAnswerForARolledOverEpochPaysNothing() (gas: 5229422)\n[PASS] test_aProofForOneEpochDoesNotPayInAnother() (gas: 5485056)\n[PASS] test_aTokenWithoutDecimalsIsTreatedAsEighteen() (gas: 339472)\n[PASS] test_anEmptyWeekDistributesNothingAndStillWaitsSevenDays() (gas: 942371)\n[PASS] test_distributeBeforeBindRevertsEvenWithFundsAndFees() (gas: 646662)\n[PASS] test_epochOnesAnswerIsRefusedForEpochTwosRequest() (gas: 5075796)\n[PASS] test_jobsWithoutIMDRevertAndDoNotStartTheClock() (gas: 833734)\n[PASS] test_rolloverOneSecondEarlyRevertsAndAnUnknownEpochIsRefused() (gas: 5037626)\n[PASS] test_theHoldingThresholdIsOneMillionWholeTokensAtSixDecimals() (gas: 2958201)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 207.88ms (386.56ms CPU time)\n\nRan 2 tests for test/invariant/SystemInvariant.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariant_everyActionBehavedAsTheBriefSays\n[PASS] invariant_imdOnlyMovesAlongTheBriefsPaths\n[PASS] invariant_noEpochPaysBeyondItsPotAndReservedIsHeld\n[PASS] invariant_tokensAreBurnedAndEthGoesToMarketing\n SystemInvariantTest invariants (runs: 256, calls: 10240, reverts: 0)\n\n╭---------------+-------------------+-------+---------+----------╮\n| Contract      | Selector          | Calls | Reverts | Discards |\n+================================================================+\n| SystemHandler | answer            | 934   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | claim             | 915   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | distribute        | 1878  | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | donateImd         | 902   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | outsiderPokes     | 868   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | requestJob        | 910   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | rollover          | 990   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | sendEth           | 953   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | toggleOraclePrice | 990   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | warp              | 900   | 0       | 0        |\n╰---------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 11155111\n  Bound result 14035\n  Bound result 6175\n  Bound result 161\n  Bound result 102\n  Bound result 2989\n  Bound result 2720\n  Bound result 1000\n  Bound result 837043\n  Bound result 936\n  Bound result 705330\n  Bound result 609235437417347470642\n  Bound result 2\n  Bound result 337594957276\n  Bound result 100\n  Bound result 19128\n  Bound result 3845\n  Bound result 111039\n  Bound result 200\n  Bound result 4203\n  Bound result 504\n  Bound result 1\n  Bound result 14274\n  Bound result 14395\n  Bound result 2000000000000000000\n  Bound result 57\n  Bound result 2989\n  Bound result 3600\n  Bound result 1646\n  Bound result 7\n  Bound result 5233\n  Bound result 439739434027456203110\n  Bound result 5387524447372987741772490061\n  Bound result 6540035830469024673\n  Bound result 109599\n  Bound result 65\n  Bound result 42\n  Bound result 9514\n  Bound result 1\n  Bound result 14506\n  Bound result 1\n  Bound result 85442\n  Bound result 3060\n  Bound result 3696\n  Bound result 1678199\n  Bound result 1\n  Bound result 1\n  Bound result 1000000000000000000000000000\n  Bound result 16728225650955\n  Bound result 997678\n  Bound result 513375262120290794817863501\n  Bound result 52715\n  Bound result 200000\n  Bound result 5000000000000000000\n  Bound result 1\n  Bound result 864000\n  Bound result 295782\n  Bound result 946\n  Bound result 97\n  Bound result 9803\n  Bound result 1819686\n  Bound result 1\n  Bound result 1\n\n[PASS] test_theHandlerReachesEveryPath() (gas: 7105173)\nLogs:\n  Bound result 0\n  Bound result 10000000000000000000\n  Bound result 1000000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1\n  Bound result 400\n  Bound result 400\n  Bound result 0\n  Bound result 0\n  Bound result 1\n  Bound result 1\n  Bound result 0\n  Bound result 3600\n  Bound result 0\n  Bound result 604800\n  Bound result 5000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 2\n  Bound result 400\n  Bound result 400\n  Bound result 0\n  Bound result 0\n  Bound result 2\n  Bound result 2\n  Bound result 2419200\n  Bound result 1\n  Bound result 1\n  Bound result 86400\n  Bound result 0\n  Bound result 0\n  Bound result 0\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.39s (2.39s CPU time)\n\nRan 8 test suites in 2.39s (2.94s CPU time): 71 tests passed, 0 failed, 9 skipped (80 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HolderRewards.claim(uint256,uint256,bytes32[])\",\"HolderRewards.deposit(uint256)\",\"HolderRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"HolderRewards.rollover(uint256)\",\"HolderRewards.setToken(address,address)\",\"JobsVault.requestJob(bytes)\",\"JobsVault.setToken(address)\",\"SIMDVault.bindLaunch(uint64,address)\",\"SIMDVault.distribute()\",\"SIMDVault.launch(bytes)\",\"SIMDVault.receive()\"],\"files\":{\".gitignore\":5,\"README.md\":149,\"foundry.toml\":15,\"remappings.txt\":2,\"src/Fixed.sol\":32,\"src/HolderRewards.sol\":296,\"src/JobsVault.sol\":100,\"src/OracleAttestation.sol\":221,\"src/OracleQuestion.sol\":255,\"src/SIMDVault.sol\":155,\"src/interfaces/IArbSys.sol\":8,\"src/interfaces/IIntake.sol\":20,\"src/interfaces/ILaunchFactory.sol\":23,\"test/Base.t.sol\":166,\"test/EdgeCases.t.sol\":210,\"test/Fork.t.sol\":138,\"test/ForkLimits.t.sol\":148,\"test/HolderRewards.t.sol\":399,\"test/JobsVault.t.sol\":128,\"test/OracleConsumerConformance.t.sol\":127,\"test/SIMDVault.t.sol\":288,\"test/invariant/SystemInvariant.t.sol\":455,\"test/mocks/MockArbSys.sol\":11,\"test/mocks/MockERC20.sol\":14,\"test/mocks/MockIntake.sol\":80,\"test/mocks/MockLaunchFactory.sol\":68,\"test/mocks/MockSignerRegistry.sol\":22},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9610774c1f9187877616924184ab12cbc7589be42b1782970f5cb730f62819d1","verifiedTreeHash":"1e67296f1637623f58e4b3000b649badd07a4fd3","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":6412,"exitCode":0,"name":"build","output":"Compiling 66 files with Solc 0.8.26\nSolc 0.8.26 finished in 6.26s\nCompiler run successful!\n","passed":true},{"durationMs":255,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/Fork.t.sol:ForkTest\n[SKIP] test_fork_bindLaunchRejectsALaunchTheVaultDidNotRequest() (gas: 0)\n[SKIP] test_fork_distributeAsksTheRealOracleAndOpensAJob() (gas: 0)\n[SKIP] test_fork_launchPaysTheRealIntakeFromTheVault() (gas: 0)\n[SKIP] test_fork_theLiveContractsAreWhatTheVaultExpects() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 4 skipped; finished in 1.29ms (462.61µs CPU time)\n\nRan 5 tests for test/OracleConsumerConformance.t.sol:OracleConsumerConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 86695)\n[PASS] test_acceptsTheProtocolSignature() (gas: 123530)\n[PASS] test_callbackSelectorIsCanonical() (gas: 3626)\n[PASS] test_digestMatchesTheProtocol() (gas: 13610)\n[PASS] test_theCallbackTakesOnlyItsOwnRequests() (gas: 39949)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.53ms (1.65ms CPU time)\n\nRan 31 tests for test/HolderRewards.t.sol:HolderRewardsTest\n[PASS] test_aBadOrForeignSignatureIsRefused() (gas: 232123)\n[PASS] test_aClaimNeedsAProofForTheCallersOwnLeaf() (gas: 304422)\n[PASS] test_aClosedEpochTakesNoClaimsAndNoSecondRollover() (gas: 2447040)\n[PASS] test_aDepositBelowThePriceIsHeldForTheNextEpoch() (gas: 2315800)\n[PASS] test_aDepositOpensAnEpochAndAsksTheOracleFromItsOwnIMD() (gas: 946309)\n[PASS] test_aDoubleClaimReverts() (gas: 385615)\n[PASS] test_aPotThinnerThanTwiceTheOraclePriceIsHeld() (gas: 2334100)\n[PASS] test_aQuietOracleBecomesDormantSixtyDaysAfterAnUnansweredDeposit() (gas: 2467219)\n[PASS] test_aRefusedAskHoldsTheDepositWithoutAnEpoch() (gas: 3000229)\n[PASS] test_aRetiredOracleActionLeavesNothingStrandedForGood() (gas: 2050564)\n[PASS] test_aRolledOverEpochWithoutARootMovesItsWholePot() (gas: 2546273)\n[PASS] test_aWeakPanelIsRefused() (gas: 178870)\n[PASS] test_aWrongAnswerTypeIsRefused() (gas: 118960)\n[PASS] test_aZeroBalanceLeafPaysNothing() (gas: 384048)\n[PASS] test_anAnswerIsStoredWithinTheStipedAndOnlyOnce() (gas: 366175)\n[PASS] test_anAnswerToAnotherQuestionIsRefused() (gas: 77662)\n[PASS] test_anExpiredAttestationIsRefused() (gas: 81992)\n[PASS] test_anOversizedClaimReverts() (gas: 395071)\n[PASS] test_fourHoldersClaimTheirShares() (gas: 657269)\n[PASS] test_headFallsBackToBlockNumberWithoutArbSys() (gas: 14769)\n[PASS] test_holdersClaimProRataOnce() (gas: 526127)\n[PASS] test_noClaimsBeforeTheRootOrOnAnUnknownEpoch() (gas: 324047)\n[PASS] test_onlyTheIntakeDeliversAnAnswer() (gas: 163511)\n[PASS] test_onlyTheVaultDeposits() (gas: 136250)\n[PASS] test_recoverDormantNeverTouchesAnOpenEpochsPot() (gas: 2565594)\n[PASS] test_rolloverIntoAClosedCurrentEpochIsCarried() (gas: 2258311)\n[PASS] test_rolloverIntoARootedEpochPassesThroughSoEqualLeavesArePaidEqually() (gas: 5007417)\n[PASS] test_rolloverMovesExactlyPotMinusClaimedIntoARootlessCurrentEpoch() (gas: 2688267)\n[PASS] test_theCurrentEpochRollsOverIntoTheCarryAfterTheDelay() (gas: 2253961)\n[PASS] test_theLatestRootedEpochOnlyMovesForward() (gas: 2541903)\n[PASS] test_theQuestionFitsTheOraclesLimitsAndNamesEveryExclusion() (gas: 565863)\nSuite result: ok. 31 passed; 0 failed; 0 skipped; finished in 12.28ms (56.74ms CPU time)\n\nRan 18 tests for test/JobsVault.t.sol:JobsVaultTest\n[PASS] test_aDormantOracleLetsLiveHoldersOpenJobsWithoutAProof() (gas: 3162341)\n[PASS] test_aFlashBorrowedBalanceProvesNothing() (gas: 161577)\n[PASS] test_aHolderOpensAJobPaidFromTheVaultsIMD() (gas: 513079)\n[PASS] test_aNewRootReplacesTheSnapshot() (gas: 3316316)\n[PASS] test_aProofForAnotherHoldersLeafIsRefused() (gas: 149099)\n[PASS] test_aProvenHolderThatSoldIsRefused() (gas: 526481)\n[PASS] test_aSnapshotHolderProvesItsLeafOnce() (gas: 135166)\n[PASS] test_aSnapshotHoldingBelowOneMillionTokensIsRefused() (gas: 164753)\n[PASS] test_aTokenWithTooManyDecimalsBindsWithTheEighteenDecimalMinimum() (gas: 815667)\n[PASS] test_anEmptyBodyAndAnUnsoldActionAreRefused() (gas: 187627)\n[PASS] test_anUnsoldJobActionIsRecoverableSixtyDaysAfterItIsNoticed() (gas: 908104)\n[PASS] test_noJobsBeforeTheFirstRootArrives() (gas: 2912868)\n[PASS] test_noJobsBeforeTheTokenIsBound() (gas: 81333)\n[PASS] test_oneHoldingOpensOneJobHoweverManyWalletsItPassesThrough() (gas: 1730054)\n[PASS] test_oneJobPerWalletPerDay() (gas: 873126)\n[PASS] test_runsDryWhenTheIMDIsSpent() (gas: 15518488)\n[PASS] test_thereIsNoWithdraw() (gas: 78385)\n[PASS] test_twoSnapshotHoldersHaveTheirOwnClocks() (gas: 3149897)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 14.24ms (31.68ms CPU time)\n\nRan 27 tests for test/SIMDVault.t.sol:SIMDVaultTest\n[PASS] testFuzz_splitConservesIMD(uint128,uint128) (runs: 256, μ: 2423057, ~: 3132055)\n[PASS] test_aFailedClaimNeverBlocksTheSplit() (gas: 2885253)\n[PASS] test_aFailedClaimWithNothingToSplitDoesNotSpendTheWeek() (gas: 3412354)\n[PASS] test_aFailedOracleAskNeverBlocksTheSplitTheBurnOrTheNativePayout() (gas: 3878944)\n[PASS] test_aMissingFactoryNeverBlocksTheSplit() (gas: 2804791)\n[PASS] test_aNativePayoutGoesToMarketingWithoutBlockingTheSplitOrTheBurn() (gas: 3126696)\n[PASS] test_aRevertingPriceReadNeverBlocksTheSplit() (gas: 925195)\n[PASS] test_aWalletThatRefusesEthDoesNotBlockTheSplit() (gas: 1040454147)\n[PASS] test_bindLaunchOnlyByTheDevWallet() (gas: 34922)\n[PASS] test_bindLaunchOnlyOnce() (gas: 200256)\n[PASS] test_bindLaunchRejectsALaunchWhoseRequesterIsNotTheVault() (gas: 196487)\n[PASS] test_bindLaunchRejectsAPoolNotPairedWithIMD() (gas: 134576)\n[PASS] test_bindLaunchRejectsATokenOutsideThePoolAndTheZeroToken() (gas: 76974)\n[PASS] test_bindLaunchRejectsIMDAsTheLaunchedToken() (gas: 61885)\n[PASS] test_bindLaunchStoresTheLaunchAndPassesTheTokenOn() (gas: 735854)\n[PASS] test_deploysItsChildrenAndHasNoOwner() (gas: 102208)\n[PASS] test_distributeAtMostOnceEverySevenDays() (gas: 5058597)\n[PASS] test_distributeClaimsSplitsAndBurns() (gas: 3177264)\n[PASS] test_distributeLeavesRoundingDustForNextWeek() (gas: 970953)\n[PASS] test_distributeOnlyAfterBindLaunch() (gas: 518146)\n[PASS] test_launchFollowsAMovedPrice() (gas: 534495)\n[PASS] test_launchOnlyByTheDevWallet() (gas: 43950)\n[PASS] test_launchOnlyOnce() (gas: 523848)\n[PASS] test_launchPaysExactPriceFromTheVaultAndMakesItThePayer() (gas: 595084)\n[PASS] test_launchRefusesAnEmptyBodyAndAnUnsoldAction() (gas: 126420)\n[PASS] test_launchRevertsWithoutEnoughIMD() (gas: 94096)\n[PASS] test_onlyTheVaultSetsTheChildrenTokens() (gas: 281263)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 104.47ms (144.15ms CPU time)\n\nRan 5 test suites in 105.44ms (133.81ms CPU time): 81 tests passed, 0 failed, 4 skipped (85 total tests)\n","passed":true},{"durationMs":75,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HolderRewards.claim(uint256,uint256,bytes32[])\",\"HolderRewards.deposit(uint256)\",\"HolderRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"HolderRewards.recoverDormant()\",\"HolderRewards.rollover(uint256)\",\"HolderRewards.setToken(address,address)\",\"JobsVault.noticeUnsold()\",\"JobsVault.proveAndRequestJob(uint256,uint256,bytes32[],bytes)\",\"JobsVault.proveHolding(uint256,uint256,bytes32[])\",\"JobsVault.recoverDormant()\",\"JobsVault.requestJob(bytes)\",\"JobsVault.setToken(address)\",\"SIMDVault.bindLaunch(uint64,address)\",\"SIMDVault.distribute()\",\"SIMDVault.launch(bytes)\",\"SIMDVault.receive()\"],\"files\":{\".gitignore\":5,\"README.md\":224,\"foundry.toml\":15,\"remappings.txt\":2,\"src/Fixed.sol\":32,\"src/HolderRewards.sol\":398,\"src/JobsVault.sol\":217,\"src/OracleAttestation.sol\":221,\"src/OracleQuestion.sol\":255,\"src/SIMDVault.sol\":164,\"src/interfaces/IArbSys.sol\":8,\"src/interfaces/IIntake.sol\":20,\"src/interfaces/ILaunchFactory.sol\":23,\"test/Base.t.sol\":166,\"test/Fork.t.sol\":195,\"test/HolderRewards.t.sol\":600,\"test/JobsVault.t.sol\":403,\"test/OracleConsumerConformance.t.sol\":127,\"test/SIMDVault.t.sol\":380,\"test/mocks/MockArbSys.sol\":11,\"test/mocks/MockERC20.sol\":14,\"test/mocks/MockIntake.sol\":80,\"test/mocks/MockLaunchFactory.sol\":68,\"test/mocks/MockSignerRegistry.sol\":22},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4003,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/SIMDVault.sol:123: SIMDVault.distribute() (src/SIMDVault.sol#123-158) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HolderRewards.sol:305: HolderRewards.recoverDormant() (src/HolderRewards.sol#305-314) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/HolderRewards.sol:155: HolderRewards.deposit(uint256) (src/HolderRewards.sol#155-165) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/JobsVault.sol:190: JobsVault.recoverDormant() (src/JobsVault.sol#190-196) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/SIMDVault.sol:123: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#123-158):\n[medium/medium] unused-return at src/SIMDVault.sol:102: SIMDVault.bindLaunch(uint64,address) (src/SIMDVault.sol#102-117) ignores return value by (key,None,None,requester) = LAUNCH_FACTORY.positionOf(launchNumber_) (src/SIMDVault.sol#107)\n[low/high] shadowing-local at src/HolderRewards.sol:135: HolderRewards.constructor(address).oracleSigner (src/HolderRewards.sol#135) shadows:\n[low/medium] missing-zero-check at src/HolderRewards.sol:142: HolderRewards.setToken(address,address).jobsVault_ (src/HolderRewards.sol#142) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/SIMDVault.sol:84: Reentrancy in SIMDVault.launch(bytes) (src/SIMDVault.sol#84-96):\n[low/medium] reentrancy-benign at src/HolderRewards.sol:171: Reentrancy in HolderRewards._openEpoch(uint256,uint256) (src/HolderRewards.sol#171-206):\n[low/medium] reentrancy-events at src/HolderRewards.sol:171: Reentrancy in HolderRewards._openEpoch(uint256,uint256) (src/HolderRewards.sol#171-206):\n[low/medium] reentrancy-events at src/JobsVault.sol:137: Reentrancy in JobsVault._requestJob(bytes) (src/JobsVault.sol#137-158):\n[low/medium] reentrancy-events at src/SIMDVault.sol:102: Reentrancy in SIMDVault.bindLaunch(uint64,address) (src/SIMDVault.sol#102-117):\n[low/medium] reentrancy-events at src/SIMDVault.sol:123: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#123-158):\n[low/medium] reentrancy-events at src/SIMDVault.sol:84: Reentrancy in SIMDVault.launch(bytes) (src/SIMDVault.sol#84-96):\n[low/medium] reentrancy-events at src/SIMDVault.sol:123: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#123-158):\n[low/medium] reentrancy-events at src/SIMDVault.sol:123: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#123-158):\n[low/medium] reentrancy-events at src/HolderRewards.sol:155: Reentrancy in HolderRewards.deposit(uint256) (src/HolderRewards.sol#155-165):\n[low/medium] timestamp at src/SIMDVault.sol:123: SIMDVault.distribute() (src/SIMDVault.sol#123-158) uses timestamp for comparisons\n[low/medium] timestamp at src/HolderRewards.sol:155: HolderRewards.deposit(uint256) (src/HolderRewards.sol#155-165) uses timestamp for comparisons\n[low/medium] timestamp at src/HolderRewards.sol:298: HolderRewards.isDormant() (src/HolderRewards.sol#298-300) uses timestamp for comparisons\n[low/medium] timestamp at src/JobsVault.sol:162: JobsVault.canRequest(address) (src/JobsVault.sol#162-168) uses timestamp for comparisons\n[low/medium] timestamp at src/JobsVault.sol:184: JobsVault.isDormant() (src/JobsVault.sol#184-186) uses timestamp for comparisons\n[low/medium] timestamp at src/HolderRewards.sol:275: HolderRewards.rollover(uint256) (src/HolderRewards.sol#275-291) uses timestamp for comparisons\n[low/medium] timestamp at src/JobsVault.sol:174: JobsVault.noticeUnsold() (src/JobsVault.sol#174-181) uses timestamp for comparisons\n[low/medium] timestamp at src/JobsVault.sol:137: JobsVault._requestJob(bytes) (src/JobsVault.sol#137-158) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAttestation.sol:157: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/OracleAttestation.sol#157-165) uses timestamp for comparisons","passed":true},{"durationMs":958,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/SIMDVault.sol:123: ETH transferred without address checks\n[high] reentrancy-state-change at src/HolderRewards.sol:308: Reentrancy: State change after external call (8 places)\n[low] internal-function-used-once at src/OracleAttestation.sol:195: Internal Function Used Only Once (2 places)\n[low] large-numeric-literal at src/JobsVault.sol:66: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/OracleQuestion.sol:117: Literal Instead of Constant (13 places)\n[low] local-variable-shadowing at src/HolderRewards.sol:135: Local Variable Shadows State Variable\n[low] require-revert-in-loop at src/OracleQuestion.sol:114: Loop Contains `require`/`revert` (2 places)\n[low] state-no-address-check at src/HolderRewards.sol:147: Address State Variable Set Without Checks\n[low] uninitialized-local-variable at src/OracleQuestion.sol:114: Uninitialized Local Variable (4 places)","passed":true},{"durationMs":3109,"exitCode":0,"name":"proof 0c08c986469a","output":"Compiling 61 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.19s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-5fbd7d7d/Proof_0c08c986469a.t.sol:RolloverProRataTest\n[PASS] test_holdersWithEqualBalancesGetEqualPayoutsAcrossARollover() (gas: 2186979)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.41ms (3.46ms CPU time)\n\nRan 1 test suite in 5.29ms (4.41ms 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":"a60ca1db144ba1fa0a8bceb926e9ab23d09ed780fcce72a39f645fa922cca065","verifiedTreeHash":"36b71911a30a1e28213bafea5a2eca85318ed966","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":4879,"exitCode":0,"name":"build","output":"Compiling 69 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.77s\nCompiler run successful!\n","passed":true},{"durationMs":2615,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/Fork.t.sol:ForkTest\n[SKIP] test_fork_bindLaunchRejectsALaunchTheVaultDidNotRequest() (gas: 0)\n[SKIP] test_fork_distributeAsksTheRealOracleAndOpensAJob() (gas: 0)\n[SKIP] test_fork_launchPaysTheRealIntakeFromTheVault() (gas: 0)\n[SKIP] test_fork_theLiveContractsAreWhatTheVaultExpects() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 4 skipped; finished in 635.49µs (141.33µs CPU time)\n\nRan 5 tests for test/ForkLimits.t.sol:ForkLimitsTest\n[SKIP] test_fork_bindLaunchRejectsAnotherRequester() (gas: 0)\n[SKIP] test_fork_launchAndBindOnlyByTheDevWalletAndOnlyOnce() (gas: 0)\n[SKIP] test_fork_onlyTheLiveIntakeDeliversAndJobsAreLimited() (gas: 0)\n[SKIP] test_fork_sevenDayLimitWithTheLiveFactory() (gas: 0)\n[SKIP] test_fork_splitBurnAndNativeEthWithTheLiveFactoryAndIntake() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 5 skipped; finished in 931.66µs (392.28µs CPU time)\n\nRan 5 tests for test/OracleConsumerConformance.t.sol:OracleConsumerConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 86674)\n[PASS] test_acceptsTheProtocolSignature() (gas: 123420)\n[PASS] test_callbackSelectorIsCanonical() (gas: 3626)\n[PASS] test_digestMatchesTheProtocol() (gas: 13677)\n[PASS] test_theCallbackTakesOnlyItsOwnRequests() (gas: 39983)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 954.87µs (1.08ms CPU time)\n\nRan 15 tests for test/JobsVault.t.sol:JobsVaultTest\n[PASS] test_aFlashBorrowedBalanceProvesNothing() (gas: 156000)\n[PASS] test_aHolderOpensAJobPaidFromTheVaultsIMD() (gas: 510847)\n[PASS] test_aNewRootReplacesTheSnapshot() (gas: 3248749)\n[PASS] test_aProofForAnotherHoldersLeafIsRefused() (gas: 149071)\n[PASS] test_aProvenHolderThatSoldIsRefused() (gas: 524357)\n[PASS] test_aSnapshotHolderProvesItsLeafOnce() (gas: 135126)\n[PASS] test_aSnapshotHoldingBelowOneMillionTokensIsRefused() (gas: 161972)\n[PASS] test_anEmptyBodyAndAnUnsoldActionAreRefused() (gas: 187564)\n[PASS] test_noJobsBeforeTheFirstRootArrives() (gas: 2873795)\n[PASS] test_noJobsBeforeTheTokenIsBound() (gas: 81253)\n[PASS] test_oneHoldingOpensOneJobHoweverManyWalletsItPassesThrough() (gas: 1702969)\n[PASS] test_oneJobPerWalletPerDay() (gas: 868966)\n[PASS] test_runsDryWhenTheIMDIsSpent() (gas: 15405991)\n[PASS] test_thereIsNoWithdraw() (gas: 78275)\n[PASS] test_twoSnapshotHoldersHaveTheirOwnClocks() (gas: 3085134)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 135.74ms (18.64ms CPU time)\n\nRan 25 tests for test/SIMDVault.t.sol:SIMDVaultTest\n[PASS] testFuzz_splitConservesIMD(uint128,uint128) (runs: 256, μ: 2433926, ~: 3102551)\n[PASS] test_aFailedClaimNeverBlocksTheSplit() (gas: 2855674)\n[PASS] test_aFailedOracleAskNeverBlocksTheSplitTheBurnOrTheNativePayout() (gas: 3844858)\n[PASS] test_aMissingFactoryNeverBlocksTheSplit() (gas: 2775220)\n[PASS] test_aNativePayoutGoesToMarketingWithoutBlockingTheSplitOrTheBurn() (gas: 3097023)\n[PASS] test_aRevertingPriceReadNeverBlocksTheSplit() (gas: 902661)\n[PASS] test_aWalletThatRefusesEthDoesNotBlockTheSplit() (gas: 1040453679)\n[PASS] test_bindLaunchOnlyByTheDevWallet() (gas: 34944)\n[PASS] test_bindLaunchOnlyOnce() (gas: 200035)\n[PASS] test_bindLaunchRejectsALaunchWhoseRequesterIsNotTheVault() (gas: 196394)\n[PASS] test_bindLaunchRejectsATokenOutsideThePoolAndTheZeroToken() (gas: 76974)\n[PASS] test_bindLaunchRejectsIMDAsTheLaunchedToken() (gas: 61808)\n[PASS] test_bindLaunchStoresTheLaunchAndPassesTheTokenOn() (gas: 735567)\n[PASS] test_deploysItsChildrenAndHasNoOwner() (gas: 102099)\n[PASS] test_distributeAtMostOnceEverySevenDays() (gas: 5019856)\n[PASS] test_distributeClaimsSplitsAndBurns() (gas: 3145333)\n[PASS] test_distributeLeavesRoundingDustForNextWeek() (gas: 948259)\n[PASS] test_distributeOnlyAfterBindLaunch() (gas: 518124)\n[PASS] test_launchFollowsAMovedPrice() (gas: 534495)\n[PASS] test_launchOnlyByTheDevWallet() (gas: 43950)\n[PASS] test_launchOnlyOnce() (gas: 523848)\n[PASS] test_launchPaysExactPriceFromTheVaultAndMakesItThePayer() (gas: 595084)\n[PASS] test_launchRefusesAnEmptyBodyAndAnUnsoldAction() (gas: 126420)\n[PASS] test_launchRevertsWithoutEnoughIMD() (gas: 94096)\n[PASS] test_onlyTheVaultSetsTheChildrenTokens() (gas: 280822)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 152.68ms (179.42ms CPU time)\n\nRan 25 tests for test/HolderRewards.t.sol:HolderRewardsTest\n[PASS] test_aBadOrForeignSignatureIsRefused() (gas: 230133)\n[PASS] test_aClaimNeedsAProofForTheCallersOwnLeaf() (gas: 277743)\n[PASS] test_aClosedEpochTakesNoClaimsAndNoSecondRollover() (gas: 2384244)\n[PASS] test_aDepositBelowThePriceIsHeldForTheNextEpoch() (gas: 2302064)\n[PASS] test_aDepositOpensAnEpochAndAsksTheOracleFromItsOwnIMD() (gas: 943999)\n[PASS] test_aDoubleClaimReverts() (gas: 358979)\n[PASS] test_aRefusedAskHoldsTheDepositWithoutAnEpoch() (gas: 2986527)\n[PASS] test_aRolledOverEpochWithoutARootMovesItsWholePot() (gas: 2503828)\n[PASS] test_aWeakPanelIsRefused() (gas: 176836)\n[PASS] test_aWrongAnswerTypeIsRefused() (gas: 116723)\n[PASS] test_aZeroBalanceLeafPaysNothing() (gas: 357360)\n[PASS] test_anAnswerIsStoredWithinTheStipedAndOnlyOnce() (gas: 337392)\n[PASS] test_anAnswerToAnotherQuestionIsRefused() (gas: 75561)\n[PASS] test_anExpiredAttestationIsRefused() (gas: 79822)\n[PASS] test_anOversizedClaimReverts() (gas: 368385)\n[PASS] test_fourHoldersClaimTheirShares() (gas: 630502)\n[PASS] test_headFallsBackToBlockNumberWithoutArbSys() (gas: 14780)\n[PASS] test_holdersClaimProRataOnce() (gas: 497206)\n[PASS] test_noClaimsBeforeTheRootOrOnAnUnknownEpoch() (gas: 297276)\n[PASS] test_onlyTheIntakeDeliversAnAnswer() (gas: 159200)\n[PASS] test_onlyTheVaultDeposits() (gas: 136184)\n[PASS] test_rolloverMovesExactlyPotMinusClaimedIntoTheCurrentEpoch() (gas: 2606087)\n[PASS] test_theCurrentEpochCannotRollOverIntoItself() (gas: 60872)\n[PASS] test_theLatestRootedEpochOnlyMovesForward() (gas: 2477611)\n[PASS] test_theQuestionFitsTheOraclesLimitsAndNamesEveryExclusion() (gas: 565753)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 152.69ms (32.64ms CPU time)\n\nRan 14 tests for test/EdgeCases.t.sol:EdgeCasesTest\n[PASS] testFuzz_theSplitIsExactForAnyBalance(uint256) (runs: 1000, μ: 2014789, ~: 1062201)\nLogs:\n  Bound result 1\n\n[PASS] testFuzz_twoHoldersNeverTakeMoreThanThePot(uint256,uint256,uint256) (runs: 1000, μ: 3309804, ~: 3329684)\nLogs:\n  Bound result 66\n  Bound result 2347\n  Bound result 7\n\n[PASS] test_aLateAnswerForARolledOverEpochPaysNothing() (gas: 5251674)\n[PASS] test_aProofForOneEpochDoesNotPayInAnother() (gas: 5512295)\n[PASS] test_aRevertingOracleAskNoLongerFreezesDistribute() (gas: 3845027)\n[PASS] test_aTokenWithoutDecimalsIsTreatedAsEighteen() (gas: 339568)\n[PASS] test_anEmptyWeekDistributesNothingAndStillWaitsSevenDays() (gas: 942299)\n[PASS] test_distributeBeforeBindRevertsEvenWithFundsAndFees() (gas: 646629)\n[PASS] test_epochOnesAnswerIsRefusedForEpochTwosRequest() (gas: 5075942)\n[PASS] test_imdCannotBeBoundAsTheLaunchedToken() (gas: 270709)\n[PASS] test_jobsWithoutIMDRevertAndDoNotStartTheClock() (gas: 3355779)\n[PASS] test_oneHoldingPassedThroughTenWalletsOpensOneJob() (gas: 4530886)\n[PASS] test_rolloverOneSecondEarlyRevertsAndAnUnknownEpochIsRefused() (gas: 5037792)\n[PASS] test_theHoldingThresholdIsOneMillionWholeTokensAtSixDecimals() (gas: 3330741)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 197.95ms (393.16ms CPU time)\n\nRan 2 tests for test/invariant/SystemInvariant.t.sol:SystemInvariantTest\n[PASS]\nSystemInvariantTest invariants:\n[PASS] invariant_everyActionBehavedAsTheBriefSays\n[PASS] invariant_imdOnlyMovesAlongTheBriefsPaths\n[PASS] invariant_noEpochPaysBeyondItsPotAndReservedIsHeld\n[PASS] invariant_tokensAreBurnedAndEthGoesToMarketing\n SystemInvariantTest invariants (runs: 256, calls: 10240, reverts: 0)\n\n╭---------------+-------------------+-------+---------+----------╮\n| Contract      | Selector          | Calls | Reverts | Discards |\n+================================================================+\n| SystemHandler | answer            | 824   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | claim             | 829   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | distribute        | 1735  | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | donateImd         | 878   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | outsiderPokes     | 890   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | requestJob        | 848   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | rollover          | 878   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | sendEth           | 815   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | toggleOraclePrice | 853   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | transferTokens    | 786   | 0       | 0        |\n|---------------+-------------------+-------+---------+----------|\n| SystemHandler | warp              | 904   | 0       | 0        |\n╰---------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1999999999999500000000001\n  Bound result 144760\n  Bound result 32\n  Bound result 6\n  Bound result 2366482869645213223\n  Bound result 887200\n  Bound result 65\n  Bound result 20324\n  Bound result 52715\n  Bound result 887200\n  Bound result 10000000000000000000\n  Bound result 1\n  Bound result 864000\n  Bound result 221523\n  Bound result 948\n  Bound result 102\n  Bound result 9557\n  Bound result 0\n  Bound result 4872\n  Bound result 810915\n  Bound result 66\n  Bound result 10\n  Bound result 7\n  Bound result 48\n  Bound result 352000\n  Bound result 6659\n  Bound result 305\n  Bound result 2\n  Bound result 512\n  Bound result 1000000\n  Bound result 86397\n  Bound result 604800\n  Bound result 745890\n  Bound result 998999999999999999001\n  Bound result 9347\n  Bound result 1590109535755964701\n  Bound result 1613155\n  Bound result 2448\n  Bound result 2664\n  Bound result 12\n\n[PASS] test_theHandlerReachesEveryPath() (gas: 7553944)\nLogs:\n  Bound result 0\n  Bound result 10000000000000000000\n  Bound result 1000000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1\n  Bound result 1000000000000000000000000\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 1\n  Bound result 1\n  Bound result 0\n  Bound result 0\n  Bound result 3600\n  Bound result 0\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 604800\n  Bound result 5000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 2\n  Bound result 1000000000000000000000000\n  Bound result 1000000000000000000000000\n  Bound result 0\n  Bound result 0\n  Bound result 2\n  Bound result 2\n  Bound result 2419200\n  Bound result 1\n  Bound result 1\n  Bound result 86400\n  Bound result 0\n  Bound result 0\n  Bound result 0\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.50s (2.51s CPU time)\n\nRan 8 test suites in 2.50s (3.14s CPU time): 86 tests passed, 0 failed, 9 skipped (95 total tests)\n","passed":true},{"durationMs":60,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HolderRewards.claim(uint256,uint256,bytes32[])\",\"HolderRewards.deposit(uint256)\",\"HolderRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"HolderRewards.rollover(uint256)\",\"HolderRewards.setToken(address,address)\",\"JobsVault.proveAndRequestJob(uint256,uint256,bytes32[],bytes)\",\"JobsVault.proveHolding(uint256,uint256,bytes32[])\",\"JobsVault.requestJob(bytes)\",\"JobsVault.setToken(address)\",\"SIMDVault.bindLaunch(uint64,address)\",\"SIMDVault.distribute()\",\"SIMDVault.launch(bytes)\",\"SIMDVault.receive()\"],\"files\":{\".gitignore\":5,\"README.md\":172,\"foundry.toml\":15,\"remappings.txt\":2,\"src/Fixed.sol\":32,\"src/HolderRewards.sol\":322,\"src/JobsVault.sol\":156,\"src/OracleAttestation.sol\":221,\"src/OracleQuestion.sol\":255,\"src/SIMDVault.sol\":157,\"src/interfaces/IArbSys.sol\":8,\"src/interfaces/IIntake.sol\":20,\"src/interfaces/ILaunchFactory.sol\":23,\"test/Base.t.sol\":166,\"test/EdgeCases.t.sol\":314,\"test/Fork.t.sol\":195,\"test/ForkLimits.t.sol\":210,\"test/HolderRewards.t.sol\":441,\"test/JobsVault.t.sol\":304,\"test/OracleConsumerConformance.t.sol\":127,\"test/SIMDVault.t.sol\":344,\"test/invariant/SystemInvariant.t.sol\":495,\"test/mocks/MockArbSys.sol\":11,\"test/mocks/MockERC20.sol\":14,\"test/mocks/MockIntake.sol\":80,\"test/mocks/MockLaunchFactory.sol\":68,\"test/mocks/MockSignerRegistry.sol\":22},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"b968c624c6fbdd9a553a3b9b94480af8f5e0ac6978f2600cf07e4b1be6b27be3","verifiedTreeHash":"0b3dce07c160b48b7aebcc2adb27b2d283eacab8","verifierVersion":"0.1.0+a26ee00d"},{"checks":[{"durationMs":2877,"exitCode":0,"name":"build","output":"Compiling 66 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.75s\nCompiler run successful!\n","passed":true},{"durationMs":178,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/Fork.t.sol:ForkTest\n[SKIP] test_fork_bindLaunchRejectsALaunchTheVaultDidNotRequest() (gas: 0)\n[SKIP] test_fork_distributeAsksTheRealOracleAndOpensAJob() (gas: 0)\n[SKIP] test_fork_launchPaysTheRealIntakeFromTheVault() (gas: 0)\n[SKIP] test_fork_theLiveContractsAreWhatTheVaultExpects() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 4 skipped; finished in 510.59µs (241.92µs CPU time)\n\nRan 5 tests for test/OracleConsumerConformance.t.sol:OracleConsumerConformanceTest\n[PASS] test_acceptsAFreshSignatureFromTheVectorKey() (gas: 86629)\n[PASS] test_acceptsTheProtocolSignature() (gas: 123442)\n[PASS] test_callbackSelectorIsCanonical() (gas: 3626)\n[PASS] test_digestMatchesTheProtocol() (gas: 13610)\n[PASS] test_theCallbackTakesOnlyItsOwnRequests() (gas: 40005)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 1.04ms (1.22ms CPU time)\n\nRan 8 tests for test/JobsVault.t.sol:JobsVaultTest\n[PASS] test_aHolderOpensAJobPaidFromTheVaultsIMD() (gas: 419237)\n[PASS] test_anEmptyBodyAndAnUnsoldActionAreRefused() (gas: 113682)\n[PASS] test_holdingBelowOneMillionTokensIsRefused() (gas: 391708)\n[PASS] test_movingTokensDoesNotResetTheClock() (gas: 760325)\n[PASS] test_noJobsBeforeTheTokenIsBound() (gas: 39631)\n[PASS] test_oneJobPerWalletPerDay() (gas: 1046440)\n[PASS] test_runsDryWhenTheIMDIsSpent() (gas: 18401462)\n[PASS] test_thereIsNoWithdraw() (gas: 78335)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 68.76ms (5.19ms CPU time)\n\nRan 22 tests for test/SIMDVault.t.sol:SIMDVaultTest\n[PASS] testFuzz_splitConservesIMD(uint128,uint128) (runs: 256, μ: 2380681, ~: 3102345)\n[PASS] test_aFailedClaimNeverBlocksTheSplit() (gas: 2855538)\n[PASS] test_aMissingFactoryNeverBlocksTheSplit() (gas: 2774998)\n[PASS] test_aNativePayoutGoesToMarketingWithoutBlockingTheSplitOrTheBurn() (gas: 3096790)\n[PASS] test_aWalletThatRefusesEthDoesNotBlockTheSplit() (gas: 1040453673)\n[PASS] test_bindLaunchOnlyByTheDevWallet() (gas: 34944)\n[PASS] test_bindLaunchOnlyOnce() (gas: 199986)\n[PASS] test_bindLaunchRejectsALaunchWhoseRequesterIsNotTheVault() (gas: 196306)\n[PASS] test_bindLaunchRejectsATokenOutsideThePoolAndTheZeroToken() (gas: 76952)\n[PASS] test_bindLaunchStoresTheLaunchAndPassesTheTokenOn() (gas: 735584)\n[PASS] test_deploysItsChildrenAndHasNoOwner() (gas: 93709)\n[PASS] test_distributeAtMostOnceEverySevenDays() (gas: 5019635)\n[PASS] test_distributeClaimsSplitsAndBurns() (gas: 3145242)\n[PASS] test_distributeLeavesRoundingDustForNextWeek() (gas: 948143)\n[PASS] test_distributeOnlyAfterBindLaunch() (gas: 518124)\n[PASS] test_launchFollowsAMovedPrice() (gas: 534406)\n[PASS] test_launchOnlyByTheDevWallet() (gas: 43994)\n[PASS] test_launchOnlyOnce() (gas: 523781)\n[PASS] test_launchPaysExactPriceFromTheVaultAndMakesItThePayer() (gas: 595084)\n[PASS] test_launchRefusesAnEmptyBodyAndAnUnsoldAction() (gas: 126464)\n[PASS] test_launchRevertsWithoutEnoughIMD() (gas: 94096)\n[PASS] test_onlyTheVaultSetsTheChildrenTokens() (gas: 280771)\nSuite result: ok. 22 passed; 0 failed; 0 skipped; finished in 68.71ms (84.03ms CPU time)\n\nRan 23 tests for test/HolderRewards.t.sol:HolderRewardsTest\n[PASS] test_aBadOrForeignSignatureIsRefused() (gas: 230221)\n[PASS] test_aClaimNeedsAProofForTheCallersOwnLeaf() (gas: 255680)\n[PASS] test_aClosedEpochTakesNoClaimsAndNoSecondRollover() (gas: 2362073)\n[PASS] test_aDepositBelowThePriceIsHeldForTheNextEpoch() (gas: 2301996)\n[PASS] test_aDepositOpensAnEpochAndAsksTheOracleFromItsOwnIMD() (gas: 944065)\n[PASS] test_aDoubleClaimReverts() (gas: 336916)\n[PASS] test_aRolledOverEpochWithoutARootMovesItsWholePot() (gas: 2481701)\n[PASS] test_aWeakPanelIsRefused() (gas: 176924)\n[PASS] test_aWrongAnswerTypeIsRefused() (gas: 116767)\n[PASS] test_aZeroBalanceLeafPaysNothing() (gas: 335297)\n[PASS] test_anAnswerIsStoredWithinTheStipedAndOnlyOnce() (gas: 294632)\n[PASS] test_anAnswerToAnotherQuestionIsRefused() (gas: 75605)\n[PASS] test_anExpiredAttestationIsRefused() (gas: 79866)\n[PASS] test_anOversizedClaimReverts() (gas: 346322)\n[PASS] test_fourHoldersClaimTheirShares() (gas: 608483)\n[PASS] test_headFallsBackToBlockNumberWithoutArbSys() (gas: 14758)\n[PASS] test_holdersClaimProRataOnce() (gas: 475165)\n[PASS] test_noClaimsBeforeTheRootOrOnAnUnknownEpoch() (gas: 275257)\n[PASS] test_onlyTheIntakeDeliversAnAnswer() (gas: 159310)\n[PASS] test_onlyTheVaultDeposits() (gas: 136206)\n[PASS] test_rolloverMovesExactlyPotMinusClaimedIntoTheCurrentEpoch() (gas: 2584004)\n[PASS] test_theCurrentEpochCannotRollOverIntoItself() (gas: 60872)\n[PASS] test_theQuestionFitsTheOraclesLimitsAndNamesEveryExclusion() (gas: 565753)\nSuite result: ok. 23 passed; 0 failed; 0 skipped; finished in 68.80ms (20.16ms CPU time)\n\nRan 5 test suites in 69.90ms (207.82ms CPU time): 58 tests passed, 0 failed, 4 skipped (62 total tests)\n","passed":true},{"durationMs":56,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"HolderRewards.claim(uint256,uint256,bytes32[])\",\"HolderRewards.deposit(uint256)\",\"HolderRewards.onOracleResult(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"HolderRewards.rollover(uint256)\",\"HolderRewards.setToken(address,address)\",\"JobsVault.requestJob(bytes)\",\"JobsVault.setToken(address)\",\"SIMDVault.bindLaunch(uint64,address)\",\"SIMDVault.distribute()\",\"SIMDVault.launch(bytes)\",\"SIMDVault.receive()\"],\"files\":{\".gitignore\":5,\"README.md\":149,\"foundry.toml\":15,\"remappings.txt\":2,\"src/Fixed.sol\":32,\"src/HolderRewards.sol\":296,\"src/JobsVault.sol\":100,\"src/OracleAttestation.sol\":221,\"src/OracleQuestion.sol\":255,\"src/SIMDVault.sol\":155,\"src/interfaces/IArbSys.sol\":8,\"src/interfaces/IIntake.sol\":20,\"src/interfaces/ILaunchFactory.sol\":23,\"test/Base.t.sol\":166,\"test/Fork.t.sol\":138,\"test/HolderRewards.t.sol\":399,\"test/JobsVault.t.sol\":128,\"test/OracleConsumerConformance.t.sol\":127,\"test/SIMDVault.t.sol\":288,\"test/mocks/MockArbSys.sol\":11,\"test/mocks/MockERC20.sol\":14,\"test/mocks/MockIntake.sol\":80,\"test/mocks/MockLaunchFactory.sol\":68,\"test/mocks/MockSignerRegistry.sol\":22},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2428,"exitCode":0,"name":"slither","output":"[medium/medium] unused-return at src/SIMDVault.sol:99: SIMDVault.bindLaunch(uint64,address) (src/SIMDVault.sol#99-112) ignores return value by (key,None,None,requester) = LAUNCH_FACTORY.positionOf(launchNumber_) (src/SIMDVault.sol#103)\n[low/high] shadowing-local at src/HolderRewards.sol:105: HolderRewards.constructor(address).oracleSigner (src/HolderRewards.sol#105) shadows:\n[low/medium] missing-zero-check at src/HolderRewards.sol:112: HolderRewards.setToken(address,address).jobsVault_ (src/HolderRewards.sol#112) lacks a zero-check on :\n[low/medium] reentrancy-benign at src/HolderRewards.sol:137: Reentrancy in HolderRewards._openEpoch(uint256,uint256) (src/HolderRewards.sol#137-156):\n[low/medium] reentrancy-benign at src/SIMDVault.sol:82: Reentrancy in SIMDVault.launch(bytes) (src/SIMDVault.sol#82-94):\n[low/medium] reentrancy-events at src/SIMDVault.sol:116: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#116-149):\n[low/medium] reentrancy-events at src/SIMDVault.sol:82: Reentrancy in SIMDVault.launch(bytes) (src/SIMDVault.sol#82-94):\n[low/medium] reentrancy-events at src/SIMDVault.sol:116: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#116-149):\n[low/medium] reentrancy-events at src/HolderRewards.sol:137: Reentrancy in HolderRewards._openEpoch(uint256,uint256) (src/HolderRewards.sol#137-156):\n[low/medium] reentrancy-events at src/SIMDVault.sol:99: Reentrancy in SIMDVault.bindLaunch(uint64,address) (src/SIMDVault.sol#99-112):\n[low/medium] reentrancy-events at src/JobsVault.sol:67: Reentrancy in JobsVault.requestJob(bytes) (src/JobsVault.sol#67-82):\n[low/medium] reentrancy-events at src/SIMDVault.sol:116: Reentrancy in SIMDVault.distribute() (src/SIMDVault.sol#116-149):\n[low/medium] timestamp at src/JobsVault.sol:85: JobsVault.canRequest(address) (src/JobsVault.sol#85-89) uses timestamp for comparisons\n[low/medium] timestamp at src/SIMDVault.sol:116: SIMDVault.distribute() (src/SIMDVault.sol#116-149) uses timestamp for comparisons\n[low/medium] timestamp at src/JobsVault.sol:67: JobsVault.requestJob(bytes) (src/JobsVault.sol#67-82) uses timestamp for comparisons\n[low/medium] timestamp at src/HolderRewards.sol:206: HolderRewards.rollover(uint256) (src/HolderRewards.sol#206-217) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAttestation.sol:157: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/OracleAttestation.sol#157-165) uses timestamp for comparisons","passed":true},{"durationMs":560,"exitCode":0,"name":"aderyn","output":"[high] eth-send-unchecked-address at src/SIMDVault.sol:116: ETH transferred without address checks\n[high] reentrancy-state-change at src/JobsVault.sol:71: Reentrancy: State change after external call (5 places)\n[low] internal-function-used-once at src/OracleAttestation.sol:195: Internal Function Used Only Once (2 places)\n[low] large-numeric-literal at src/JobsVault.sol:39: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/OracleQuestion.sol:117: Literal Instead of Constant (13 places)\n[low] local-variable-shadowing at src/HolderRewards.sol:105: Local Variable Shadows State Variable\n[low] require-revert-in-loop at src/OracleQuestion.sol:114: Loop Contains `require`/`revert` (2 places)\n[low] state-no-address-check at src/HolderRewards.sol:117: Address State Variable Set Without Checks\n[low] uninitialized-local-variable at src/OracleQuestion.sol:114: Uninitialized Local Variable (4 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"c5e5a95283a5e00a1320e0140be1beea3a89d460b3f23f23f018bba93c2b4d19","verifiedTreeHash":"1228f08f543ca543f6dd504710662f23ef75b2de","verifierVersion":"0.1.0+a26ee00d"}]}