{"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":"e6177e2b-abaa-4a75-9fb4-b0c46fe646fb","kind":"shape:fan_out_join","nodes":[{"acceptedSubmissionHash":"a8e1a99b13c01297677e444fb99f9bfda555c28423e7814e1861cf8737ebc3d9","dependsOn":["audit_specialist","audit_specialist_2","audit_specialist_3","audit_specialist_4"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"982df5255212e0c53551cb57c4073a72cb8214870b653ffc1806254ed1670360","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_specialist","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"5c7dde72d17cb3bbb740c444f493b398d12b4ce0302808bbc2b0b536acc5301f","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_specialist_2","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"0f8776a7e73857ca0e7be0fe1a4768344759de54353d5554556b60e3da2b0e92","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_specialist_3","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"7de38345cc013856df60a40ac8c71f242e77410f92b87ce8cdf26277c1dd2631","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_specialist_4","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"FINANCIER / FINCR final predeployment security audit. Review EXACT immutable public commit 0c866371fb280cf91df04ad2c5a85a412d998246 of https://github.com/imd-financier/financier-open.git; cite that SHA in every report. If inaccessible or any checked-out SHA differs, STOP and report BLOCKED. Do not review a moving branch, earlier token-only snapshot or substitute commit. Do not edit/deploy contracts, spend money, contact third parties or launch anything.\n\nProduction candidate: src/FINCR.sol; FounderFundedMining.sol and inherited FinancierMiningWithSettlement / FinancierMiningWithMarket / FinancierMining; RobinhoodJobController.sol; PublisherGateway.sol v2; SeatOwnerAgentRounds.sol / AttestedAgentRounds.sol; PermanentV4Market.sol; all transitively used interfaces, libraries, dependencies and auxiliary contracts. Use the public architecture/inventory to distinguish production candidates from legacy receiver/round/controller variants. No protocol contracts are claimed deployed. Canonical chain Robinhood 4663. Founder/deployer/initializer/gateway administrator 0xbcF19c0821A245383c127570615896Abac9e424F; Operations ONLY 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e; operator 0x174818c4FEcB65472522Ff8C7677F5A567cB3020. Immutable controller publisher must be the separate gateway, never operator EOA.\n\nPriority 1: Asset custody and accounting: unauthorized FINCR reserve withdrawal/diversion; IMD theft/refunds/insolvency; double claims, replay and liabilities; reentrancy, rounding and griefing; permanent liquidity custody, buyback-burn and LP safety; immutable errors stranding 400M FINCR.\nPriority 2: Gateway/controller/agent payouts: PublisherGateway v2 founder permissions versus restricted operator; initially paused and one-time controller/hash binding before miner binding; deployment order; dishonest or compromised trusted publisher; duplicate canonical agents, seats and ownerOf substitution; stale proof, cutoff/grace and replay; quarantine and unclaimable allocations.\nPriority 3: Dutch-auction economics: fixed 10 IMD opening and 0.5 IMD floor after one hour; genesis and takeover; payment waterfall/refunds; 80% tenure emissions independent of work and separate equal 20% agent rounds; work-budget accounting; MEV, flash loans, spot-price risks, economic farming and denial of service.\nPriority 4: End-to-end deployment/operation: exact founder permissions, constructors and immutables; gateway -> controller -> gateway binding -> miner -> controller binding; authentic FINCR runtime/supply/500M pool/100M swarm/400M remainder verification; exact 400M funding and arm; first bid, official scoped IMD task, accepted-work settlement and equal independent claims; canonical dependency runtime and source/test reproducibility.\n\nReview authentic IMD path: Robinhood Intake Requested/request body/job binding -> official job records -> Ethereum WorkRegistry committed work document -> accepted signals.agentId -> submission seat tokenId -> original NFT ownerOf payout snapshot. Trusted publisher relays and checks evidence OFF CHAIN. There is NO trustless Ethereum-to-Robinhood bridge; evaluate publisher omissions, false inclusions and wallet substitution under that disclosed trust assumption. NFT ownership alone proves no qualifying work. Only accepted FINANCIER-supporting work qualifies. Financier rewards remain independent. Mixed failed/successful period jobs can block standard resolver finalization; expiry quarantines allocations. Check these actual policies rather than inventing capabilities.\n\nOne audit-specialist reviews each of the four assigned areas, followed by audit-judge reproducing and merging all four reports within THIS ONE job. Preserve source exactly; propose fixes as findings only. Include Critical / High / Medium / Low classification, exact source file and line, practical preconditions and plausible impact, and reproducible proof/exploit/test where appropriate. Judge must independently distinguish reproduced defects from unsupported reviewer assertions, duplicates, assumptions and accepted limitations. Give final GO / FIX FIRST / NO-GO verdict plus prioritized blockers. Verify build, tests, invariants/exploit regressions and deployment rehearsal as documented, and disclose any checks not run. No invented audit guarantees, production automation, deployed addresses or launch success.\n\nThis paid prelaunch project review is NOT a mining-round job and earns NO postlaunch 20% agent emission allocation. All real signing, payment, deployment, reserve transfer, unpause, site release and hackathon submission require separate founder approval.","parentJobId":null,"planHash":"7e2f1f10d0d95a854ef8167756f45188a3dc778404604e8bc734b9f887714240","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"e6177e2b-abaa-4a75-9fb4-b0c46fe646fb","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51130","feedbackHash":"22ae937180c3aa5458993ae5ba1dc0906bb60df6d5a734db858e130265267efc","nodeKey":"audit_judge","submissionHash":"a8e1a99b13c01297677e444fb99f9bfda555c28423e7814e1861cf8737ebc3d9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52642","feedbackHash":"67e31d1ef97f6ecf18d429c764a9cdbb82063eff77565ba6d84fd142b6d62794","nodeKey":"audit_specialist","submissionHash":"982df5255212e0c53551cb57c4073a72cb8214870b653ffc1806254ed1670360","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51566","feedbackHash":"4a2c5f4ca22d3fbde952ad6768389df23ae35ff06a4e55b86a6c5f40046f0b0b","nodeKey":"audit_specialist_2","submissionHash":"5c7dde72d17cb3bbb740c444f493b398d12b4ce0302808bbc2b0b536acc5301f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52207","feedbackHash":"15f7a138fc0df8b6f149004a8a27bba8c33c988d5eb2261d54f00f4cbcc88915","nodeKey":"audit_specialist_3","submissionHash":"0f8776a7e73857ca0e7be0fe1a4768344759de54353d5554556b60e3da2b0e92","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52609","feedbackHash":"ba7503b5370bf6776a4337bf4db8864363a71bd6f2ddef36bb512ad36a112365","nodeKey":"audit_specialist_4","submissionHash":"7de38345cc013856df60a40ac8c71f242e77410f92b87ce8cdf26277c1dd2631","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"4bfe486323d3003885e013614fe01de679a9cfa794fdf2f10e1c16f58a798fc6","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f009de0ce52c7b3a","findings":[{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. RobinhoodJobController.finalizeRound (src/RobinhoodJobController.sol:138-144) refuses to finalize a round unless EVERY purchased request of that round has been bound through bindJob (requestRoundPlusOne == round+1). But FinancierMiningWithSettlement.dispatchWork (src/FinancierMiningWithSettlement.sol:122) accepts a dispatch at any block.timestamp < c.end, while AttestedAgentRounds.bindJob (src/AttestedAgentRounds.sol:240) also requires block.timestamp < c.end and additionally needs the backend jobId/admission first. A request dispatched at c.end-1 (or any time closer to the window end than backend admission latency) can therefore never be bound, so finalizeRound reverts InvalidSettlement forever for that round, regardless of how many other jobs were bound, completed and have accepted agents. After the one-day grace, anyone calls expire(id) and the full agent budget of the round (TOKENOMICS: separate equal 20% stream) is quarantined. The trigger is unprivileged: the current financier is whoever last bid (0.5 IMD at the floor, 80% of which is refunded on the next takeover). commission() spends the protocol work budget, and the attacker can top it up with fundWork(0.5 ether) if needed. In era 1 one 24h round carries 1,959,183.67 FINCR of agent allocation (agentAllocation(1 day, 2 days) on the 400M reserve), so each 0.5 IMD of work spend denies ~1.96M FINCR to every agent accepted in that round and permanently strands it in the vault (requiredFincrCustody never releases quarantined amounts). The same failure happens without malice whenever an honest financier commissions work in the last minutes of a day and the backend admission lands after midnight. TRUST_AND_LIMITATIONS.md only discloses that failed jobs block the standard resolver; an unbound-but-completed purchase is not disclosed and is enforced on chain, not just off chain. Fix options (any makes the proof pass): close dispatchWork/commission a safety margin before c.end (e.g. c.end - graceSeconds or a fixed cutoff) so every purchase can still be bound, or let finalizeRound tolerate purchased requests that are recorded but unbound by the window end (they cannot carry accepted agents because permittedJob requires bindJob), or allow bindJob during the grace period for requests dispatched inside the window.","line":141,"path":"src/RobinhoodJobController.sol","reproduction":"State: FounderFundedMining armed and genesis bid placed by ALICE (floor 0.5 IMD), rounds = 1 day, grace = 1 day. 1) ALICE.fundWork(1 IMD); ALICE calls controller.commission(0,0) -> request H; publisher bindJob(0, H, JOB); intake completes H with status 0. 2) warp to context(0).end - 1; ALICE (still financier) calls controller.commission(0,1) -> request L; intake completes L successfully. 3) warp to context(0).end; publisher bindJob(0, L, ...) reverts WrongTime (bindJob requires timestamp < end). 4) publisher lockSnapshot(0, s) succeeds; publisher finalizeRound(0, [valid accepted contribution for JOB], s, owners, commitment). Expected: round 0 finalizes, count 1, OWNER claims share > 0. Actual: finalizeRound reverts InvalidSettlement at src/RobinhoodJobController.sol:142 because requestRoundPlusOne(L) == 0; the revert is permanent, and after context(0).deadline anyone can expire(0), quarantining the entire round-0 agent budget. Proof test test/scratch/LateDispatchBlocksRound.t.sol fails on this commit with InvalidSettlement(); a variant with step 2 removed passes end to end (finalize + claim), isolating the late dispatch as the cause.","severity":"high","snippet":"            if (!recorded || miner.agentRounds().requestRoundPlusOne(purchased[i]) != round + 1) {\n                revert InvalidSettlement();\n            }","title":"Financier can make any 24h agent round unfinalizable by commissioning work in the last seconds of the window; the whole 20% agent budget of that round is quarantined"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. precommitRound refuses to commit a scope once the round has started (src/AttestedAgentRounds.sol:210). Only round 0 is prepared on chain (FounderFundedMining.arm -> jobs.prepareRound(0), src/FounderFundedMining.sol:64). Every later 24h round depends on someone calling RobinhoodJobController.prepareRound(id) before genesis + id*1 day. If that call is missed for a day, scopes[id].committed stays false: dispatchWork reverts InvalidWorkIntent (body not permitted), bindJob reverts ScopeUnavailable, finalize reverts ScopeUnavailable, and the only reachable terminal state is expire(id), which quarantines the whole agent allocation for that day (1,959,183.67 FINCR in era 1, 979,591.84 in era 2, 489,795.92 in era 3, ~170,836 per day in the tail). No contract, script or documented production automation guarantees the daily call (README: no unattended automation is claimed; TRUST_AND_LIMITATIONS only says period zero is prepared at arm). The 20% stream promised in TOKENOMICS is therefore conditional on a daily keeper transaction with no on-chain fallback. Suggested fix: let prepareRound(id) be callable for any id whose window has started but not yet ended (bodies are deterministic and contain the round number, so late precommit adds no publisher discretion), or have arm()/bid()/dispatchWork lazily precommit the current and next round.","line":210,"path":"src/AttestedAgentRounds.sol","reproduction":"State: armed miner, genesis bid at t0, nobody calls prepareRound(1) before t0 + 1 day. At block.timestamp = context(1).start: controller.prepareRound(1) reverts WrongTime; rounds.scopes(1).committed == false; financier commission(1,0) reverts (dispatchWork InvalidWorkIntent). At context(1).deadline + 1: rounds.expire(1) succeeds and totalQuarantined == agentAllocation(1 day, 2 days) == 1959183673469387755102041 wei FINCR. Reproduced in scratch test testMissedPrepareRoundQuarantinesDay (log: quarantined FINCR for round 1 (wei): 1959183673469387755102041). Expected: a round whose scope is deterministic should still be preparable/usable once started, or the allocation should roll forward; actual: the day's agent emissions are permanently stranded.","severity":"medium","snippet":"        if (vault.epoch() != 0 && block.timestamp >= vault.genesis() + id * roundSeconds) revert WrongTime();","title":"Missed prepareRound(id) before a round starts permanently quarantines that day's ~1.96M FINCR agent budget; only round 0 is prepared on chain"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. FounderFundedMining.onResult reverts InvalidCompletion for a result URI longer than 2048 bytes or a zero result hash (src/FounderFundedMining.sol:97), and onFailure reverts for any status other than 1 or 2 (src/FounderFundedMining.sol:102). Against the pinned Robinhood Intake runtime (test/fixtures/task16/intake-runtime.hex, codehash pinned in src/FinancierMiningWithSettlement.sol:18), a callback revert does not fail the writer's complete() call: the intake marks requests(id).completed = true, a second complete() for the same id reverts, and completions[id].recorded stays false forever. RobinhoodJobController.finalizeRound requires completions[...].recorded for every purchased and every bound request (src/RobinhoodJobController.sol:140-149), so the round containing that request can never be finalized and its whole 20% agent budget (about 1.96M FINCR per day in era 1) is quarantined on expire. The trigger is a benign backend output rather than an attacker (the IMD service is a disclosed trusted dependency), but the on-chain guard is self-inflicted: the miner only stores keccak256(bytes(uri)) and the status byte, so there is no need to reject long URIs or unusual statuses at callback time. Whether an agent can influence the result URI length through its artifact naming is not determinable from this tree. Suggested fix: record every callback from the pinned intake for a known request (store uriHash and the raw status, flag it as unusable for acceptance) instead of reverting, so finalizeRound can still close the round; keep acceptance of agents gated on status 0 and a non-zero result as today.","line":97,"path":"src/FounderFundedMining.sol","reproduction":"State: armed miner, genesis bid by ALICE, ALICE commission(0,0) -> request R (round 0). Intake writer calls complete(R, 0, keccak256('result'), <2100-byte uri>, abi.encode(R, hash, uri)). Observed on the pinned intake runtime in a scratch test: complete() succeeds; IIMDIntakeViews(INTAKE).requests(R).completed == true; miner.completions(R).recorded == false; a retry complete(R, 0, hash, 'uri', ...) reverts; recorded stays false. Consequence: controller.finalizeRound(0, ...) reverts InvalidSettlement at src/RobinhoodJobController.sol:141 (purchased R not recorded) for the rest of the grace period; after context(0).deadline anyone can expire(0) and the full round-0 agent allocation is quarantined. Expected: a completion the miner cannot accept should still be recorded so other accepted jobs in the round can settle.","severity":"medium","snippet":"        if (resultHash == 0 || bytes(uri).length == 0 || bytes(uri).length > 2048) revert InvalidCompletion();","title":"Miner-side callback rejection (URI > 2048 bytes, zero result hash, unexpected failure status) leaves the request completed at the intake but never recorded, permanently blocking finalization of that r"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. bid() does not distinguish msg.sender == iFinancier (src/FinancierMining.sol:139-163) and _allocate credits 80% of the price back to the outgoing financier (src/FinancierMining.sol:174-181), which FinancierMiningWithSettlement pays out automatically in the same transaction. A self-bid at live price P therefore costs the incumbent only 0.2P net while resetting the quote to 10 IMD, whereas a challenger must pay P. With a 1h decay from 10 to 0.5 IMD, an incumbent bot that resets whenever the price reaches 0.5 IMD holds the slot for 0.1 IMD/hour (about 877 IMD/year) and captures the entire 80% financier stream (320M FINCR), and can additionally front-run any observed challenger bid with a same-block self-bid so the challenger's bid reverts StaleEpoch (src/FinancierMining.sol:141). Disallowing msg.sender == iFinancier does not help because a second wallet gives the same 0.2P cost. Existing tests (AutoOutgoing testSelfBidSpendsOnlyNetWaterfallButDoesNotInflateEmissions, Auction testSameBlockSelfTakeoversNoEmissionsAndPartialRecovery) show the authors know self-bids are allowed and cost 20% net, but neither the docs nor TOKENOMICS disclose that the incumbent's defense is five times cheaper than attack, which makes the 'takeover' design effectively first-bidder-wins. Documented as 'no incoming challenger guaranteed', so reported as low: no funds are lost to the protocol, but the distribution guarantee implied by the Dutch auction is not delivered. Possible mitigations: withhold the outgoing refund (or part of it) for self/short tenures, or a minimum tenure, which TOKENOMICS currently rules out.","line":180,"path":"src/FinancierMining.sol","reproduction":"State: armed, ALICE genesis bid, warp 1 hour so currentPrice()==0.5 IMD. Challenger BOB submits bid(1, 0.5e18, deadline). In the same block, ALICE submits bid(1, 0.5e18, deadline) first: ALICE pays 0.5 IMD, receives 0.4 IMD back via payOutgoingExact (net cost 0.1 IMD), epoch becomes 2, resetPrice = 10 IMD. BOB's bid then reverts StaleEpoch. Repeating every hour at the floor keeps ALICE financier for 0.1 IMD/hour; BOB can only win by paying up to 10 IMD immediately after a reset, of which 8 IMD is paid to ALICE. Expected per TOKENOMICS: a price-discovery auction between financiers; actual: incumbent pays one fifth of the challenger's price to keep the slot.","severity":"low","snippet":"            outgoingCredit[previous] += outgoing;\n            totalOutgoingCredits += outgoing;","title":"Incumbent financier can pre-empt any challenger at 20% of the challenger's price (self-takeover refund asymmetry), defeating Dutch-auction price discovery"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. EmissionSchedule.gross returns the full reserve for elapsed >= 365 days, so no financier accrual exists after genesis + 365 days, but bid() (src/FinancierMining.sol:139-163) has no emission-end check. A bidder after the end pays the live quote (0.5 to 10 IMD), 20% is routed to work/buyback/LP/operations and 80% to the previous financier, and the bidder's claimableFincr stays 0 forever. The only way to recover anything is for yet another late bidder to take over. The quote decaying from 10 IMD after every takeover also keeps signalling an active auction. Suggested fix: revert bids once elapsed() >= EMISSION_END (or when the remaining financier allocation is zero), or clearly surface it in currentPrice()/UI.","line":139,"path":"src/FinancierMining.sol","reproduction":"State: armed, ALICE genesis bid. warp to genesis + 365 days + 1 hour. New EOA (909) with 1 IMD calls bid(1, 0.5e18, block.timestamp). Actual: 0.5 IMD is debited (0.4 IMD to ALICE, 0.1 IMD into buckets), epoch 2, and after 30 more days claimableFincr(909) == 0 and currentTenureEarned() == 0. Expected: a bid that can never earn emissions should be refused. Reproduced in scratch test testBidAfterEmissionEndPaysForNothing.","severity":"low","snippet":"    function bid(uint256 expectedEpoch, uint256 maxPrice, uint256 deadline) external nonReentrant {\n        if (!armed) revert NotArmed();\n        if (expectedEpoch != epoch) revert StaleEpoch();","title":"bid() accepts payments after EMISSION_END: a late bidder pays the live quote and can never earn any FINCR"}],"hash":"0f8776a7e73857ca0e7be0fe1a4768344759de54353d5554556b60e3da2b0e92","nodeId":"27ad033c-8c40-4216-9083-a5849324fc0a","outcome":"completed","summary":"All findings are substantiated, snippets match the tree, and the proof still fails on this commit. Nothing further depends on another result, so the review is complete.\n\n## Review of commit `0c866371fb280cf91df04ad2c5a85a412d998246`, Priority 3: Dutch-auction economics\n\nChecked-out HEAD matches the required SHA and the remote is the named repository. The report is in `.imd-findings.json` with five findings. Verdict for my area: **FIX FIRST**, driven by the first finding.\n\n**Findings, most severe first**\n\n- **High. A financier can make any 24h agent round unfinalizable** by commissioning work in the last seconds of the window. The controller's `finalizeRound` requires every purchased request to be bound, but binding needs the same window end that dispatch allows. The round can then only expire, quarantining its full 20% agent budget (about 1.96M FINCR per day in era 1). The trigger is unprivileged (any floor bidder) and spends protocol work budget. Proof test in `test/scratch/LateDispatchBlocksRound.t.sol` fails with `InvalidSettlement` now; the same flow without the late dispatch passes end to end.\n- **Medium. A missed `prepareRound(id)` before a day starts strands that day's agent budget.** Only round 0 is prepared on chain, and precommit is refused once the round has begun. Reproduced: expire quarantines 1,959,183.67 FINCR for round 1.\n- **Medium. A callback the miner rejects (URI over 2048 bytes, zero hash, odd status) is marked complete by the pinned intake runtime but never recorded**, and the writer cannot retry. The round containing it can never be finalized. Reproduced against the fixture runtime. Trigger is the trusted backend, so medium.\n- **Low. Incumbent self-takeover costs 20% of what a challenger pays**, so the holder can reset the quote or front-run a challenger at one fifth of the price. The authors test self-bids but the asymmetry is not disclosed.\n- **Low. Bids are accepted after emissions end** and the bidder can never earn FINCR.\n\n**Coverage of the assigned area**\n\n| Entry point | Status |\n|---|---|\n| `bid`, `currentPrice`, genesis and takeover, reset to 10 IMD, floor | holds, except the two low findings |\n| `_allocate` waterfall, dust, genesis 85% to work | holds, matches TOKENOMICS |\n| Automatic outgoing and operations payout, fallback credit, gas caps | holds |\n| `claimFincr`, `_checkpoint`, 80% tenure accrual independent of work | holds |\n| Agent 20% stream budget, telescoping sum, `payAgentReward` bound | holds |\n| `dispatchWork`, `fundWork`, work-budget isolation | finding 1 (timing) |\n| `prepareRound` liveness | finding 2 |\n| Completion callbacks | finding 3 |\n| Market spot bounds, sandwich, batch cap | holds as a documented limitation, exposure bounded at 0.1 IMD per bucket per bid |\n| Reentrancy across bid, claims, rounds | holds via shared guard |\n\n**Checks run and not run.** `forge build` and the full `forge test` suite ran: 257 passed, 0 failed. I did not run the extended fuzz profile, the fork rehearsal, or any deployment step. Whether the IMD token owner can disable transfers (the fixture calls `enableTransfers()`) was not assessed and belongs to the custody area.","treeHash":null,"usage":{"cachedInputTokens":3077092,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":50676,"runtime":"claude","turns":46,"wallClockMs":760180}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d0653dc91b6e2259","findings":[{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. RobinhoodJobController.finalizeRound requires every request purchased for the round (purchasedRequests) to be both completion-recorded and bound via bindJob (requestRoundPlusOne == round+1). commission(round, category) is callable by the current iFinancier at any block.timestamp < c.end, and it spends the protocol work budget, not the financier's own IMD. AttestedAgentRounds.bindJob (line 240) refuses binding once block.timestamp >= c.end. A commission submitted in the last seconds of the period (or any time the backend cannot admit before c.end) therefore produces a purchased request that can never be bound, finalizeRound reverts InvalidSettlement for the entire period, and after the grace period anyone calls expire(): every honest agent already accepted in the other job(s) of that period loses their share, which is quarantined permanently (round 0 budget = 1,959,183 FINCR). The financier bears no cost beyond 0.5 IMD of protocol work budget and is not a trusted role. The same blocking applies to up to three categories per period. The disclosed limitation in TRUST_AND_LIMITATIONS.md covers failed jobs in the off-chain resolver, not an on-chain veto by the bidder. Suggested fix: refuse commission when block.timestamp + admission margin >= c.end, or let finalizeRound treat a purchased request that was never bound and whose requestedAt is after a cutoff as void (it was already paid and consumed).","line":141,"path":"src/RobinhoodJobController.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {AttestedAgentRounds as Rounds} from \"src/AttestedAgentRounds.sol\";\nimport {IAgentWorkVerifier as W} from \"src/interfaces/IAgentWorkVerifier.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ninterface ISeatIntake {\n    function setPrice(bytes32, address, uint256) external;\n    function setPayTo(address) external;\n    function setWriter(address) external;\n    function setCallbackGas(uint64) external;\n    function complete(bytes32, uint8, bytes32, string calldata, bytes calldata) external;\n}\n\n/// A current financier commissions a second job one second before period end. The publisher\n/// cannot admit/bind it before `c.end` (bindJob requires block.timestamp < c.end), so\n/// RobinhoodJobController.finalizeRound reverts for the whole period and the honest agent's\n/// already-accepted work is quarantined on expiry. Fails on this code; passes once a late\n/// commission is either refused or no longer able to block settlement of the other jobs.\ncontract LateCommissionBlocksRoundTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant ALICE = address(101);\n    address constant OWNER = address(301);\n    address constant ATTESTER = address(700);\n    bytes32 constant JOB = keccak256(\"SYNTHETIC FINANCIER job UUID\");\n    PublisherGateway gateway;\n    FINCR token;\n    FounderFundedMining miner;\n    RobinhoodJobController controller;\n    Rounds rounds;\n    bytes32 request;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(0x8366a39CC670B4001A1121B8F6A443A643e40951, vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\")));\n        vm.etch(0x19Bec7C2E1b2aadAf67b259744751a9960d66000, vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\")));\n        vm.store(IMD, 0, bytes32(uint256(uint160(address(this)))));\n        (bool ok,) = IMD.call(abi.encodeWithSignature(\"enableTransfers()\"));\n        require(ok);\n        vm.store(INTAKE, 0, bytes32(uint256(uint160(address(this)))));\n        ISeatIntake(INTAKE).setPrice(\"job.open@job-1\", IMD, 0.5 ether);\n        ISeatIntake(INTAKE).setPayTo(address(800));\n        ISeatIntake(INTAKE).setWriter(address(this));\n        ISeatIntake(INTAKE).setCallbackGas(200_000);\n        token = new FINCR(1e27);\n        vm.prank(FOUNDER);\n        gateway = new PublisherGateway(ATTESTER);\n        vm.prank(FOUNDER);\n        controller = new RobinhoodJobController(address(gateway));\n        vm.prank(FOUNDER);\n        gateway.bindController(controller, address(controller).codehash);\n        vm.prank(FOUNDER);\n        gateway.setPaused(false);\n        FinancierMiningWithMarket.Config memory c = FinancierMiningWithMarket.Config(\n            address(token), IMD, 1e27, 10 ether, 0.5 ether, FOUNDER, 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e,\n            address(controller), controller.SPECIFICATION(), 1 days, 1 days, 1 hours, 0.1 ether\n        );\n        vm.prank(FOUNDER);\n        miner = new FounderFundedMining(c);\n        vm.prank(FOUNDER);\n        controller.bindMiner(INativeJobMining(address(miner)));\n        rounds = miner.agentRounds();\n        deal(IMD, ALICE, 100 ether);\n        vm.prank(ALICE);\n        IERC20(IMD).approve(address(miner), type(uint256).max);\n        token.transfer(address(miner), 4e26);\n        vm.prank(FOUNDER);\n        miner.arm();\n        vm.prank(ALICE);\n        miner.bid(0, 10 ether, block.timestamp);\n    }\n\n    function testLateCommissionCannotQuarantineHonestAcceptedWork() public {\n        // Honest path: job commissioned, admitted and bound early in the period, completed successfully.\n        vm.prank(ALICE);\n        request = controller.commission(0, 0);\n        bytes32 bodyHash = keccak256(controller.body(0, 0));\n        vm.prank(ATTESTER);\n        gateway.bindJob(0, Rounds.Admission(request, JOB, bodyHash, block.timestamp, block.timestamp, keccak256(\"admission\")));\n        vm.warp(block.timestamp + 10);\n        bytes32 hash = keccak256(\"SYNTHETIC nonempty completed result\");\n        ISeatIntake(INTAKE).complete(request, 0, hash, \"uri\", abi.encode(request, hash, \"uri\"));\n\n        W.Context memory c = rounds.context(0);\n        // Financier (any bidder) commissions another category one second before period end.\n        // Work budget (protocol IMD) pays; the publisher cannot bind it before c.end.\n        vm.warp(c.end - 1);\n        vm.prank(ALICE);\n        try controller.commission(0, 1) {} catch {}\n\n        // Period closes; publisher locks snapshot and finalizes with the one honest accepted agent.\n        vm.warp(c.end);\n        Rounds.Snapshot memory s = Rounds.Snapshot(\n            26_000_000, keccak256(\"hdr\"), c.end - 15 minutes, keccak256(\"owners\"), keccak256(\"accepted\")\n        );\n        vm.prank(ATTESTER);\n        gateway.lockSnapshot(0, s);\n        W.Contribution[] memory a = new W.Contribution[](1);\n        Rounds.OwnerEvidence[] memory o = new Rounds.OwnerEvidence[](1);\n        uint256 boundAt = rounds.jobBoundAt(JOB);\n        a[0] = W.Contribution(\n            1, rounds.IDENTITY_REGISTRY(), 51_000, OWNER, OWNER, JOB, keccak256(\"receipt\"), keccak256(\"submission\"),\n            keccak256(\"artifact\"), boundAt + 2, s.timestamp, c.deadline\n        );\n        o[0] = Rounds.OwnerEvidence(100, false, 0, 0, boundAt + 1);\n        bytes32 commitment = rounds.settlementCommitment(0, a, s, o);\n        vm.prank(ATTESTER);\n        gateway.finalizeRound(0, a, s, o, commitment); // reverts InvalidSettlement on current code\n        vm.prank(OWNER);\n        assertEq(rounds.claim(0), miner.agentAllocation(0, 1 days));\n    }\n}","reproduction":"1) Deploy gateway/controller/miner as in test/Task30Gateway.t.sol, arm, ALICE bids (genesis). 2) ALICE commission(0,0); operator binds JOB; intake completes the request with status 0 and nonzero hash. 3) vm.warp(c.end - 1); ALICE calls commission(0,1) -> succeeds (workBudget -= 0.5 IMD). 4) vm.warp(c.end); operator lockSnapshot(0, s) succeeds; operator finalizeRound(0, [honest agent], s, owners, commitment). Expected: round finalizes and OWNER can claim agentAllocation(0,1 day). Actual: revert InvalidSettlement() at line 141-143 because requestRoundPlusOne(secondRequest) == 0; the only remaining path is expire(0) which quarantines the full budget. Proof: test/scratch/LateCommissionBlocksRound.t.sol fails with InvalidSettlement() on this commit.","severity":"medium","snippet":"            if (!recorded || miner.agentRounds().requestRoundPlusOne(purchased[i]) != round + 1) {\n                revert InvalidSettlement();\n            }","title":"Any current financier can block a whole period's agent settlement by commissioning a job too late for admission"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. The documented order (ARCHITECTURE.md, LAUNCH.md step 4) is gateway -> controller -> gateway.bindController -> miner -> controller.bindMiner. On-chain this order is enforced only in one direction: PublisherGateway.bindController (line 50) rejects a controller whose miner() is already set, but RobinhoodJobController.bindMiner (lines 47-52) and FounderFundedMining.arm (line 63) never check that PublisherGateway(publisher).controller() == address(this). If the founder calls bindMiner before bindController, bindController reverts InvalidConfiguration forever, the controller's immutable publisher (the gateway) can never call bindJob/lockSnapshot/finalizeRound, yet the miner constructor, bindMiner, 400M funding and arm all succeed. Every agent round can then only expire(); 80,000,000 FINCR (the 20% agent share of the 400M reserve) is permanently stranded in the miner, which has no rescue path. This is a founder procedural error, but all other ordering invariants (epoch()==0, miner()==0, codehash, policyHash) are enforced on-chain and the gateway binding is the one that is not. Suggested fix: in bindMiner require IGateway(publisher).controller() == address(this) (publisher is a contract by policy), or check the same in arm().","line":47,"path":"src/RobinhoodJobController.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {AttestedAgentRounds as Rounds} from \"src/AttestedAgentRounds.sol\";\n\n/// Deployment order is only enforced one way: PublisherGateway.bindController rejects a controller\n/// that already has a miner, but RobinhoodJobController.bindMiner and FounderFundedMining.arm never\n/// check that the immutable publisher (the gateway) has actually been bound to this controller.\n/// If the founder binds the miner first, the gateway can never bind, no publisher call can ever\n/// reach the controller, every agent round expires to quarantine, and 80M FINCR (20% of the\n/// 400M reserve) is stranded in the miner with no recovery path. Fails on this code; passes once\n/// bindMiner (or arm) refuses while the gateway is not bound to this controller.\ncontract MisorderedBindingStrandsAgentsTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant OPERATOR = 0x174818c4FEcB65472522Ff8C7677F5A567cB3020;\n\n    function testMinerCannotArmWhileGatewayIsUnboundToController() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(0x8366a39CC670B4001A1121B8F6A443A643e40951, vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\")));\n        vm.etch(0x19Bec7C2E1b2aadAf67b259744751a9960d66000, vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\")));\n        FINCR token = new FINCR(1e27);\n        vm.prank(FOUNDER);\n        PublisherGateway gateway = new PublisherGateway(OPERATOR);\n        vm.prank(FOUNDER);\n        RobinhoodJobController controller = new RobinhoodJobController(address(gateway));\n        FinancierMiningWithMarket.Config memory c = FinancierMiningWithMarket.Config(\n            address(token), IMD, 1e27, 10 ether, 0.5 ether, FOUNDER, 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e,\n            address(controller), controller.SPECIFICATION(), 1 days, 1 days, 1 hours, 0.1 ether\n        );\n        vm.prank(FOUNDER);\n        FounderFundedMining miner = new FounderFundedMining(c);\n\n        // Wrong order: miner bound before the gateway binding.\n        vm.prank(FOUNDER);\n        try controller.bindMiner(INativeJobMining(address(miner))) {} catch {}\n\n        // Gateway binding is now impossible forever (bindController requires candidate.miner() == 0).\n        vm.prank(FOUNDER);\n        try gateway.bindController(controller, address(controller).codehash) {} catch {}\n\n        token.transfer(address(miner), 4e26);\n        vm.prank(FOUNDER);\n        try miner.arm() {} catch {}\n\n        // Nobody can ever call the controller as publisher: the gateway holds no controller,\n        // and the controller's immutable publisher is the gateway, not an EOA.\n        bool publisherReachable = address(gateway.controller()) == address(controller);\n        // Defect: the reserve is armed while the agent settlement path is unreachable.\n        assertFalse(miner.armed() && !publisherReachable, \"armed with permanently unreachable publisher\");\n    }\n}","reproduction":"1) vm.prank(FOUNDER): gateway = new PublisherGateway(OPERATOR); controller = new RobinhoodJobController(gateway); miner = new FounderFundedMining(config with authority=controller). 2) vm.prank(FOUNDER): controller.bindMiner(miner) -> succeeds. 3) vm.prank(FOUNDER): gateway.bindController(controller, controller.codehash) -> reverts InvalidConfiguration (miner()!=0). 4) transfer 400M FINCR, vm.prank(FOUNDER) miner.arm() -> succeeds, armed()==true while gateway.controller()==address(0). Expected: step 2 or 4 refuses while the publisher gateway is unbound. Actual: reserve armed with an unreachable settlement path. Proof: test/scratch/MisorderedBindingStrandsAgents.t.sol fails with 'armed with permanently unreachable publisher' on this commit.","severity":"medium","snippet":"        if (\n            address(miner) != address(0) || address(candidate).code.length == 0\n                || candidate.initializer() != initializer || address(candidate.jobs()) != address(this)\n                || candidate.epoch() != 0 || candidate.agentRounds().settlementAuthority() != address(this)\n                || candidate.agentRounds().policyHash() != SPECIFICATION\n        ) revert InvalidBinding();","title":"bindMiner and arm do not verify the gateway is bound to the controller; miner-first ordering strands the 20% agent stream"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. FounderFundedMining.onResult reverts for resultHash == 0, empty uri or uri longer than 2048 bytes, and onFailure reverts for any status other than 1 or 2. The pinned Robinhood Intake v2 runtime (test/fixtures/task16/intake-runtime.hex) marks the request completed even when the payer callback reverts and refuses a second complete() for the same request (verified locally: requests(id).completed == true, recorded == false, retry reverts). RobinhoodJobController.finalizeRound (lines 139-149) requires completions(request).recorded for every purchased and bound request, so one such official result makes the period unfinalizable; after grace it expires and the whole agent budget is quarantined, including shares earned on other jobs in that period. The trigger is a benign backend output shape (a long result URI, or a failure code outside {1,2}), not an attacker. Suggested fix: never revert inside the official callback; record keccak256(uri) regardless of length (it is already stored as uriHash) and record any status value, leaving acceptance to the publisher (finalizeRound already requires status == 0 and result != 0).","line":97,"path":"src/FounderFundedMining.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {AttestedAgentRounds as Rounds} from \"src/AttestedAgentRounds.sol\";\nimport {IIMDIntakeViews} from \"src/interfaces/IIMDIntakeViews.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ninterface ISeatIntake {\n    function setPrice(bytes32, address, uint256) external;\n    function setPayTo(address) external;\n    function setWriter(address) external;\n    function setCallbackGas(uint64) external;\n    function complete(bytes32, uint8, bytes32, string calldata, bytes calldata) external;\n}\n\n/// The pinned Intake runtime marks a request completed even when the payer callback reverts and\n/// refuses a second completion. FounderFundedMining.onResult reverts for any uri longer than 2048\n/// bytes (and for a zero resultHash; onFailure reverts for any status other than 1/2), so one such\n/// official result leaves `completions[request].recorded == false` forever. finalizeRound then\n/// reverts for the whole period and its agent budget is quarantined on expiry. Fails on this code;\n/// passes once the callback records every official completion instead of reverting.\ncontract RevertingCallbackStrandsRoundTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant ALICE = address(101);\n    address constant ATTESTER = address(700);\n    bytes32 constant JOB = keccak256(\"SYNTHETIC FINANCIER job UUID\");\n    PublisherGateway gateway;\n    FINCR token;\n    FounderFundedMining miner;\n    RobinhoodJobController controller;\n    Rounds rounds;\n    bytes32 request;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(0x8366a39CC670B4001A1121B8F6A443A643e40951, vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\")));\n        vm.etch(0x19Bec7C2E1b2aadAf67b259744751a9960d66000, vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\")));\n        vm.store(IMD, 0, bytes32(uint256(uint160(address(this)))));\n        (bool ok,) = IMD.call(abi.encodeWithSignature(\"enableTransfers()\"));\n        require(ok);\n        vm.store(INTAKE, 0, bytes32(uint256(uint160(address(this)))));\n        ISeatIntake(INTAKE).setPrice(\"job.open@job-1\", IMD, 0.5 ether);\n        ISeatIntake(INTAKE).setPayTo(address(800));\n        ISeatIntake(INTAKE).setWriter(address(this));\n        ISeatIntake(INTAKE).setCallbackGas(200_000);\n        token = new FINCR(1e27);\n        vm.prank(FOUNDER);\n        gateway = new PublisherGateway(ATTESTER);\n        vm.prank(FOUNDER);\n        controller = new RobinhoodJobController(address(gateway));\n        vm.prank(FOUNDER);\n        gateway.bindController(controller, address(controller).codehash);\n        vm.prank(FOUNDER);\n        gateway.setPaused(false);\n        FinancierMiningWithMarket.Config memory c = FinancierMiningWithMarket.Config(\n            address(token), IMD, 1e27, 10 ether, 0.5 ether, FOUNDER, 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e,\n            address(controller), controller.SPECIFICATION(), 1 days, 1 days, 1 hours, 0.1 ether\n        );\n        vm.prank(FOUNDER);\n        miner = new FounderFundedMining(c);\n        vm.prank(FOUNDER);\n        controller.bindMiner(INativeJobMining(address(miner)));\n        rounds = miner.agentRounds();\n        deal(IMD, ALICE, 100 ether);\n        vm.prank(ALICE);\n        IERC20(IMD).approve(address(miner), type(uint256).max);\n        token.transfer(address(miner), 4e26);\n        vm.prank(FOUNDER);\n        miner.arm();\n        vm.prank(ALICE);\n        miner.bid(0, 10 ether, block.timestamp);\n        vm.prank(ALICE);\n        request = controller.commission(0, 0);\n        bytes32 bodyHash = keccak256(controller.body(0, 0));\n        vm.prank(ATTESTER);\n        gateway.bindJob(0, Rounds.Admission(request, JOB, bodyHash, block.timestamp, block.timestamp, keccak256(\"admission\")));\n        vm.warp(block.timestamp + 10);\n    }\n\n    function testOfficialResultWithLongUriMustStillBeRecorded() public {\n        bytes memory longUri = new bytes(2049);\n        for (uint256 i; i < longUri.length; ++i) longUri[i] = \"a\";\n        bytes32 hash = keccak256(\"SYNTHETIC nonempty completed result\");\n        // Official writer completes the request once; the pinned runtime does not revert when the\n        // payer callback reverts, and it refuses any second completion of the same request.\n        ISeatIntake(INTAKE).complete(request, 0, hash, string(longUri), abi.encode(request, hash, string(longUri)));\n        (,,, bool completedAtIntake) = IIMDIntakeViews(INTAKE).requests(request);\n        assertTrue(completedAtIntake, \"intake marked complete\");\n        vm.expectRevert();\n        ISeatIntake(INTAKE).complete(request, 0, hash, \"uri\", abi.encode(request, hash, \"uri\"));\n        (bool recorded,,,) = miner.completions(request);\n        assertTrue(recorded, \"official completion must be recorded so the period can settle\");\n    }\n}","reproduction":"1) Setup as in test/Task30Gateway.t.sol (pinned IMD + Intake runtimes, writer = test). 2) ALICE commission(0,0); operator binds JOB. 3) Writer calls INTAKE.complete(request, 0, nonzeroHash, uri of 2049 'a' bytes, abi.encode(request, hash, uri)). Expected: completion recorded (or at least retryable). Actual: INTAKE.requests(request).completed == true, miner.completions(request).recorded == false, and a second INTAKE.complete(request, 0, hash, 'uri', ...) reverts. finalizeRound(0, ...) then reverts InvalidSettlement at line 148 forever; expire(0) quarantines 1,959,183 FINCR. Proof: test/scratch/RevertingCallbackStrandsRound.t.sol fails with 'official completion must be recorded so the period can settle'.","severity":"medium","snippet":"        if (resultHash == 0 || bytes(uri).length == 0 || bytes(uri).length > 2048) revert InvalidCompletion();\n        _record(requestId, 0, resultHash, uri);","title":"Official completion callback that reverts (uri > 2048 bytes, zero hash, unexpected failure status) leaves the job permanently unrecorded and blocks the period"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. arm() precommits only round 0. AttestedAgentRounds.precommitRound (line 210) reverts WrongTime once block.timestamp >= genesis + id*roundSeconds, so prepareRound(id) must be called before each round starts. Nothing in bid(), commission(), bindJob() or finalize() prepares the next round, and no automation is supplied or claimed (TRUST_AND_LIMITATIONS.md only states 'Period zero scope is prepared at arm'). For an unprepared round, commission reverts (permittedRequestBody false), bindJob/finalize revert ScopeUnavailable, and the only outcome is expire() -> full quarantine (round 1 budget = 1,959,183 FINCR). Mitigation exists (anyone may call prepareRound for all 365 rounds immediately after arm) but is not enforced. Suggested fix: have arm() or the first bid()/commission() of round n prepare rounds n+1 (and optionally more) so liveness never depends on an external caller.","line":78,"path":"src/RobinhoodJobController.sol","reproduction":"1) Setup and start as in test/Task30Gateway.t.sol (round 0 prepared at arm). 2) vm.warp(rounds.context(1).start) without calling prepareRound(1). 3) controller.prepareRound(1) -> revert WrongTime; ALICE commission(1,0) -> revert InvalidWorkIntent. 4) vm.warp(context(1).deadline+1); rounds.expire(1) succeeds and totalQuarantined grows by agentAllocation(1 day, 2 days). Verified locally in a scratch test (testMissedPrepareRoundStrandsRound).","severity":"low","snippet":"    function prepareRound(uint256 round) public {\n        AttestedAgentRounds rounds = miner.agentRounds();\n        (bool committed,) = rounds.scopes(round);\n        if (committed) return;\n        bytes32[] memory hashes = new bytes32[](3);\n        for (uint256 i; i < 3; ++i) {\n            hashes[i] = keccak256(body(round, i));\n        }\n        rounds.precommitRound(round, hashes, SPECIFICATION);\n    }","title":"Scope precommit for rounds >= 1 relies on unspecified public automation; a missed prepareRound strands that round's agent budget"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. PublisherGateway v2 gives the founder pause/rotate powers as the mitigation for operator-key compromise, but lockSnapshot (line 115: block.timestamp >= c.end) and finalizeRound carry no minimum separation, and finalize in AttestedAgentRounds (line 299) allows block.timestamp == c.end. A compromised operator can lock a fabricated snapshot and finalize an invented accepted set (e.g. itself as the only controller/beneficiary) in the same block at exactly c.end, then claim immediately; the founder has no window to observe the locked snapshot and pause. This is within the disclosed trust boundary (operator can misallocate the 20% stream), so it is reported as a mitigation gap, not a new loss class. Suggested fix: require finalizeRound at least N seconds/blocks after the SnapshotLocked event for the round (bounded by the grace period).","line":132,"path":"src/RobinhoodJobController.sol","reproduction":"Setup as in test/Task30Gateway.t.sol; complete the bound job; vm.warp(c.end); vm.startPrank(ATTESTER): gateway.lockSnapshot(0,s); gateway.finalizeRound(0, [agent with controller=beneficiary=ATTESTER], s, owners, commitment); rounds.claim(0) -> returns 1,959,183 FINCR in the same block (verified locally, testLockAndFinalizeSameBlock). Expected per gateway design: a founder review window exists between lock and settlement.","severity":"low","snippet":"        if (snapshotDigest[round] == 0 || snapshotDigest[round] != keccak256(abi.encode(snapshot))) {\n            revert InvalidSettlement();\n        }","title":"No on-chain delay between lockSnapshot and finalizeRound: founder pause cannot intercept a compromised operator's atomic settlement"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. The task policy and README state the controller publisher must be the separate gateway contract, never the operator EOA. RobinhoodJobController's constructor accepts any nonzero publisher, bindMiner does not inspect it, and FounderFundedMining's constructor only rejects jobs.publisher() == c.operations. A controller deployed with publisher = 0x174818c4FEcB65472522Ff8C7677F5A567cB3020 (the operator EOA) passes construction, bindMiner and arm (test/Task27DSeatOwner.t.sol does exactly this with an EOA attester). In that configuration the operator holds unpausable, unrotatable publisher power for the full year and the gateway can never be inserted (publisher is immutable). Suggested fix: require jobs.publisher().code.length != 0 and PublisherGateway(jobs.publisher()).FOUNDER() == c.initializer in the miner constructor or in bindMiner.","line":43,"path":"src/FounderFundedMining.sol","reproduction":"vm.prank(FOUNDER): controller = new RobinhoodJobController(0x174818c4FEcB65472522Ff8C7677F5A567cB3020); miner = new FounderFundedMining(config with authority=controller); controller.bindMiner(miner); fund 400M; miner.arm(). Expected: rejected as a policy violation. Actual: all succeed; the operator EOA is the immutable publisher.","severity":"low","snippet":"            c.authority.code.length == 0 || jobs.initializer() != c.initializer || jobs.publisher() == c.operations\n                || c.policy != jobs.SPECIFICATION() || address(jobs.miner()) != address(0)\n        ) revert InvalidCanonicalConfiguration();","title":"'Publisher must be the gateway, never the operator EOA' is not enforced on-chain; miner constructor only rejects publisher == operations"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. grep shows PublisherGateway is referenced by exactly one test file (test/Task30Gateway.t.sol), and the production candidate FounderFundedMining only by Task27BGenesis, Task27DSeatOwner and Task30Gateway. test/IntegrationRehearsal.t.sol and test/IntegrationInvariant.t.sol (via test/helpers/IntegrationBase.sol) deploy the historical ReserveForwarder with mocks, and the Task27D seat-owner suite binds an EOA attester as publisher, so the documented production topology (gateway as immutable publisher, paused start, one-time binding, operator forwarding) is covered by a single unit suite and by no invariant run or deployment rehearsal. Coverage note for the judge; no on-chain defect by itself.","line":66,"path":"test/Task30Gateway.t.sol","reproduction":"Run: grep -l PublisherGateway test/*.sol test/helpers/*.sol -> only test/Task30Gateway.t.sol; grep -l FounderFundedMining test/*.sol -> Task27BGenesis, Task27DSeatOwner, Task30Gateway; grep -n 'new ' test/helpers/IntegrationBase.sol -> ReserveForwarder + mocks. Expected: the deployment rehearsal reproduces LAUNCH.md steps 4-5 (gateway-bound controller, FounderFundedMining). Actual: it rehearses the legacy forwarder.","severity":"info","snippet":"        gateway = new PublisherGateway(ATTESTER);\n        vm.prank(FOUNDER);\n        controller = new RobinhoodJobController(address(gateway));\n        vm.prank(FOUNDER);\n        gateway.bindController(controller, address(controller).codehash);","title":"Only Task30Gateway exercises the gateway->controller path; integration rehearsal and invariants bind an EOA publisher"}],"hash":"5c7dde72d17cb3bbb740c444f493b398d12b4ce0302808bbc2b0b536acc5301f","nodeId":"8eb6e253-5251-49a3-a0be-e0b35e87443f","outcome":"completed","summary":"Review complete. Findings are in `.imd-findings.json` at the repository root, all against commit `0c866371fb280cf91df04ad2c5a85a412d998246` (verified as HEAD of the pinned remote).\n\n**Result for the Gateway/controller/agent-payout area: FIX FIRST.** No fund theft beyond the disclosed trusted-publisher boundary, but three reproducible ways a period's entire 20% agent budget is quarantined without any dishonest publisher.\n\nFindings written, each with exact file, line and copied snippet:\n\n1. **Medium, RobinhoodJobController.sol:141.** Any current financier can veto a period's agent settlement by calling `commission` seconds before period end. The request is paid from protocol work budget, can never be bound before `c.end`, and `finalizeRound` then reverts for the whole period. Honest accepted agents lose their shares to quarantine. Proof test fails with `InvalidSettlement()`.\n2. **Medium, RobinhoodJobController.sol:47.** `bindMiner` and `arm` never check that the gateway is bound to the controller. Binding the miner first makes `bindController` impossible forever, yet funding and arm succeed, stranding 80M FINCR of agent emissions. Proof test fails on this code.\n3. **Medium, FounderFundedMining.sol:97.** The pinned Intake runtime marks a request complete even when the payer callback reverts and refuses retries. A result URI over 2048 bytes, a zero hash, or a failure status outside {1,2} leaves the job permanently unrecorded, so the period can never finalize. Proof test fails on this code.\n4. **Low.** Scope precommit for rounds 1 and later relies on an unspecified public caller. A missed `prepareRound` strands that round's budget. Reproduced locally.\n5. **Low.** No delay between `lockSnapshot` and `finalizeRound`. A compromised operator locks, finalizes and claims in one block, so the founder pause is reactive only. Reproduced locally, within the disclosed trust boundary.\n6. **Low.** The \"publisher must be the gateway\" rule is not enforced on-chain. A controller with the operator EOA as publisher passes construction, binding and arm.\n7. **Info.** Only one unit suite exercises the gateway topology. The integration rehearsal and invariants deploy the legacy forwarder with mocks.\n\nCoverage of entry points in the area. Holds: gateway constructor, `setPaused`, `replaceOperator`, `bindController` one-shot and hash check, operator-only forwarding, controller `bindJob` duplicate and dispatch-record checks, `lockSnapshot` cutoff and freshness bounds, rounds `finalize` in-round duplicate agent and seat rejection, cross-round receipt, submission and per-agent artifact replay guards, redirect rejection, contract-owner quarantine, `expire` and `claim` accounting, `payAgentReward` bound. Findings: `commission` timing, `bindMiner` ordering, `onResult` and `onFailure`, `prepareRound` liveness. Unreached: EIP-7702 delegated owner wallets on Robinhood, and the Intake callback gas limit versus `_record` cost.\n\nChecks run: forge build passes. The full existing suite passes with 257 tests and 0 failures, including invariants at the default profile. Not run: the extended fuzz profile, Slither or Mythril, the build-pin verification script, and any live-chain rehearsal.","treeHash":null,"usage":{"cachedInputTokens":2111170,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":48960,"runtime":"claude","turns":36,"wallClockMs":848677}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8e25456c4ae69acf","findings":[{"citation":"resolved","description":"Reviewed commit 0c866371fb280cf91df04ad2c5a85a412d998246. RobinhoodJobController.finalizeRound requires every request purchased in the round to be both completed AND bound via bindJob (requestRoundPlusOne == round+1). commission()/dispatchWork (src/FinancierMiningWithSettlement.sol:122) accept a purchase up to c.end-1, but AttestedAgentRounds.bindJob (src/AttestedAgentRounds.sol:240) only accepts bindings while block.timestamp < c.end and requires off-chain backend admission (admittedAt <= block.timestamp). A purchase made in the last seconds of a period therefore can never be bound, so finalizeRound reverts InvalidSettlement for that round forever and the only remaining path is expire(), which quarantines the entire period budget permanently (no sweep/redirect). The current financier is not trusted: anyone becomes financier by bidding (as low as the 0.5 IMD floor), and the 0.5 IMD job fee comes from the shared work bucket, not the attacker. An honest financier commissioning late causes the same loss accidentally. This is distinct from the disclosed off-chain 'mixed failed/successful' resolver refusal: here every job succeeded and the honest publisher/operator has no on-chain way to settle. In era 1 a 1-day period's agent share is ~2M FINCR (20% of 4/7*240M/14). Proposed fix (finding only): stop commission/dispatchWork at c.end minus an admission margin, and/or let finalizeRound tolerate purchased requests that were never admitted while the window was open (e.g. require them to be recorded completions only), so a late purchase cannot veto already-accepted work.","line":141,"path":"src/RobinhoodJobController.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {AttestedAgentRounds as Rounds} from \"src/AttestedAgentRounds.sol\";\nimport {IAgentWorkVerifier as W} from \"src/interfaces/IAgentWorkVerifier.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ninterface ISeatIntake {\n    function setPrice(bytes32, address, uint256) external;\n    function setPayTo(address) external;\n    function setWriter(address) external;\n    function setCallbackGas(uint64) external;\n    function complete(bytes32, uint8, bytes32, string calldata, bytes calldata) external;\n}\n\n/// The current financier can void a whole period's agent allocation, including\n/// already-accepted honest work, by commissioning one more job in the period's last second.\ncontract LateCommissionGriefTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant ALICE = address(101);\n    address constant OWNER = address(301);\n    address constant ATTESTER = address(700);\n    bytes32 constant JOB = keccak256(\"SYNTHETIC honest job\");\n    PublisherGateway gateway;\n    FINCR token;\n    FounderFundedMining miner;\n    RobinhoodJobController controller;\n    Rounds rounds;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(\n            0x8366a39CC670B4001A1121B8F6A443A643e40951,\n            vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\"))\n        );\n        vm.etch(\n            0x19Bec7C2E1b2aadAf67b259744751a9960d66000,\n            vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\"))\n        );\n        vm.store(IMD, 0, bytes32(uint256(uint160(address(this)))));\n        (bool ok,) = IMD.call(abi.encodeWithSignature(\"enableTransfers()\"));\n        require(ok);\n        vm.store(INTAKE, 0, bytes32(uint256(uint160(address(this)))));\n        ISeatIntake(INTAKE).setPrice(\"job.open@job-1\", IMD, 0.5 ether);\n        ISeatIntake(INTAKE).setPayTo(address(800));\n        ISeatIntake(INTAKE).setWriter(address(this));\n        ISeatIntake(INTAKE).setCallbackGas(200_000);\n        token = new FINCR(1e27);\n        vm.startPrank(FOUNDER);\n        gateway = new PublisherGateway(ATTESTER);\n        controller = new RobinhoodJobController(address(gateway));\n        gateway.bindController(controller, address(controller).codehash);\n        gateway.setPaused(false);\n        miner = new FounderFundedMining(\n            FinancierMiningWithMarket.Config(\n                address(token),\n                IMD,\n                1e27,\n                10 ether,\n                0.5 ether,\n                FOUNDER,\n                0x74421cdb9e6ad159358c4af7C59CFAE472A0541e,\n                address(controller),\n                controller.SPECIFICATION(),\n                1 days,\n                1 days,\n                1 hours,\n                0.1 ether\n            )\n        );\n        controller.bindMiner(INativeJobMining(address(miner)));\n        vm.stopPrank();\n        rounds = miner.agentRounds();\n        deal(IMD, ALICE, 100 ether);\n        vm.prank(ALICE);\n        IERC20(IMD).approve(address(miner), type(uint256).max);\n    }\n\n    function _complete(bytes32 req) internal {\n        bytes32 hash = keccak256(\"SYNTHETIC nonempty completed result\");\n        ISeatIntake(INTAKE).complete(req, 0, hash, \"uri\", abi.encode(req, hash, \"uri\"));\n    }\n\n    function testLastSecondCommissionVoidsHonestRound() public {\n        token.transfer(address(miner), 4e26);\n        vm.prank(FOUNDER);\n        miner.arm();\n        vm.prank(ALICE);\n        miner.bid(0, 10 ether, block.timestamp); // genesis; 8.5 IMD work budget\n        // Honest, fully bound and successfully completed FINANCIER job.\n        vm.prank(ALICE);\n        bytes32 request = controller.commission(0, 0);\n        bytes32 body0 = keccak256(controller.body(0, 0));\n        vm.prank(ATTESTER);\n        gateway.bindJob(0, Rounds.Admission(request, JOB, body0, block.timestamp, block.timestamp, keccak256(\"adm\")));\n        vm.warp(block.timestamp + 10);\n        _complete(request);\n\n        W.Context memory c = rounds.context(0);\n        // Financier (anyone who bid; here the same ALICE) commissions in the last second of the period.\n        vm.warp(c.end - 1);\n        vm.prank(ALICE);\n        try controller.commission(0, 1) returns (bytes32 late) {\n            _complete(late); // even a successful callback does not help\n            vm.warp(c.end); // admission window closed: the honest publisher can no longer bind it\n            bytes32 body1 = keccak256(controller.body(0, 1));\n            vm.prank(ATTESTER);\n            try gateway.bindJob(\n                0,\n                Rounds.Admission(\n                    late,\n                    keccak256(\"late job\"),\n                    body1,\n                    c.end - 1,\n                    c.end - 1,\n                    keccak256(\"adm\")\n                )\n            ) {} catch {}\n        } catch {}\n        vm.warp(c.end);\n\n        // Honest settlement of the already-accepted agent.\n        Rounds.Snapshot memory s = Rounds.Snapshot(\n            26_000_000, keccak256(\"hdr\"), c.end - 15 minutes, keccak256(\"owners\"), keccak256(\"accepted\")\n        );\n        W.Contribution[] memory a = new W.Contribution[](1);\n        Rounds.OwnerEvidence[] memory o = new Rounds.OwnerEvidence[](1);\n        a[0] = W.Contribution(\n            1,\n            rounds.IDENTITY_REGISTRY(),\n            51_000,\n            OWNER,\n            OWNER,\n            JOB,\n            keccak256(\"receipt\"),\n            keccak256(\"submission\"),\n            keccak256(\"artifact\"),\n            rounds.jobBoundAt(JOB) + 2,\n            s.timestamp,\n            c.deadline\n        );\n        o[0] = Rounds.OwnerEvidence(100, false, 0, 0, rounds.jobBoundAt(JOB) + 1);\n        vm.prank(ATTESTER);\n        gateway.lockSnapshot(0, s);\n        bytes32 commitment = rounds.settlementCommitment(0, a, s, o);\n        vm.prank(ATTESTER);\n        // Current code: reverts InvalidSettlement because the late request is unbound; the round\n        // can then only expire, quarantining the entire period-0 agent allocation forever.\n        gateway.finalizeRound(0, a, s, o, commitment);\n\n        uint256 budget = miner.agentAllocation(0, 1 days);\n        assertEq(rounds.claimable(0, OWNER), budget);\n        vm.prank(OWNER);\n        assertEq(rounds.claim(0), budget);\n    }\n}","reproduction":"Canonical setup (chainid 4663, pinned IMD/Intake runtimes, gateway->controller->bindController->miner->bindMiner, 400M funded, arm, roundSeconds=1 days, grace=1 days, maxAge=1 hours). 1) ALICE bids 10 IMD (genesis, workBudget 8.5 IMD) and calls controller.commission(0,0); operator binds it via gateway.bindJob; Intake completes it successfully. 2) vm.warp(context(0).end - 1); ALICE calls controller.commission(0,1) -> succeeds (second 0.5 IMD request). Intake may even complete it successfully. 3) At c.end the operator's gateway.bindJob for that request reverts WrongTime. 4) gateway.lockSnapshot then gateway.finalizeRound with one valid accepted agent (OWNER) and correct commitment. Expected: finalization succeeds, OWNER can claim agentAllocation(0,1 days). Actual: finalizeRound reverts InvalidSettlement (controller line 141); after the deadline only expire(0) works and the whole period-0 budget is quarantined with 0 paid. `forge test --match-path test/scratch/LateCommissionGrief.t.sol` fails with InvalidSettlement(); replacing the late commission with a refused one (simulating the fix) makes it pass.","severity":"medium","snippet":"            if (!recorded || miner.agentRounds().requestRoundPlusOne(purchased[i]) != round + 1) {","title":"Current financier can void a whole period's 20% agent allocation, including already-accepted honest work, by commissioning a job in the period's final second"},{"citation":"resolved","description":"Reviewed commit 0c866371fb280cf91df04ad2c5a85a412d998246. The required order is gateway -> controller(publisher=gateway) -> gateway.bindController -> miner -> controller.bindMiner. PublisherGateway.bindController (src/PublisherGateway.sol) only accepts a controller whose miner() is still zero, and the controller publisher/miner and the rounds' settlementAuthority are all immutable/one-shot. Yet neither the FounderFundedMining constructor (lines 42-45: it only rejects publisher == operations) nor arm() (line 63: it only checks jobs.miner()==this) checks that jobs.publisher() is a contract whose controller() is this controller, or that it is not the operator EOA. If the founder binds the miner before the gateway, gateway.bindController reverts forever, the gateway can never forward bindJob/lockSnapshot/finalizeRound, and every round can only expire: the whole 20% agent stream (80M FINCR) is permanently quarantined in the miner after the 400M transfer (no rescue). Likewise a controller deployed with publisher = operator EOA 0x1748...3020 (explicitly forbidden by the launch policy, which requires the separate gateway) passes all constructor/bindMiner/arm checks, removing the founder pause/rotation controls. These are deployment-operator errors, but arm() is the last on-chain gate before irreversible custody and the code already enforces other launch invariants on-chain there. Proposed fix (finding only): in the constructor or arm() require jobs.publisher().code.length != 0 and PublisherGateway(jobs.publisher()).controller() == jobs (and optionally FOUNDER == initializer).","line":63,"path":"src/FounderFundedMining.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\n\n/// arm() is the last on-chain gate before the irreversible 400M custody, yet it never checks that the\n/// controller's immutable publisher is a gateway bound to that controller. A misordered deployment\n/// (controller.bindMiner before gateway.bindController) or an EOA publisher arms successfully and the\n/// whole 20% agent stream (80M FINCR) can never be settled.\ncontract UnboundGatewayArmTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant OPERATIONS = 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e;\n    address constant OPERATOR = 0x174818c4FEcB65472522Ff8C7677F5A567cB3020;\n    FINCR token;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(\n            0x8366a39CC670B4001A1121B8F6A443A643e40951,\n            vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\"))\n        );\n        vm.etch(\n            0x19Bec7C2E1b2aadAf67b259744751a9960d66000,\n            vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\"))\n        );\n        token = new FINCR(1e27);\n        token.transfer(FOUNDER, 4e26);\n    }\n\n    function _config(address controller) internal view returns (FinancierMiningWithMarket.Config memory) {\n        return FinancierMiningWithMarket.Config(\n            address(token),\n            IMD,\n            1e27,\n            10 ether,\n            0.5 ether,\n            FOUNDER,\n            OPERATIONS,\n            controller,\n            RobinhoodJobController(controller).SPECIFICATION(),\n            1 days,\n            1 days,\n            1 hours,\n            0.1 ether\n        );\n    }\n\n    /// Tries the remaining launch steps; returns whether the miner ended up funded AND armed.\n    function _deployFundArm(RobinhoodJobController controller) internal returns (bool) {\n        vm.startPrank(FOUNDER);\n        FounderFundedMining miner;\n        try new FounderFundedMining(_config(address(controller))) returns (FounderFundedMining m) {\n            miner = m;\n        } catch {\n            vm.stopPrank();\n            return false;\n        }\n        try controller.bindMiner(INativeJobMining(address(miner))) {}\n        catch {\n            vm.stopPrank();\n            return false;\n        }\n        token.transfer(address(miner), 4e26); // irreversible: no rescue path exists\n        try miner.arm() {}\n        catch {\n            vm.stopPrank();\n            return false;\n        }\n        vm.stopPrank();\n        return miner.armed();\n    }\n\n    function testMisorderedGatewayBindingStillArms() public {\n        vm.startPrank(FOUNDER);\n        PublisherGateway gateway = new PublisherGateway(OPERATOR);\n        RobinhoodJobController controller = new RobinhoodJobController(address(gateway));\n        vm.stopPrank();\n        // Gateway binding skipped/misordered: miner is bound first.\n        bool armed = _deployFundArm(controller);\n        // Gateway can now never bind the controller (requires controller.miner()==0) -> no relay path.\n        vm.prank(FOUNDER);\n        vm.expectRevert(PublisherGateway.InvalidConfiguration.selector);\n        gateway.bindController(controller, address(controller).codehash);\n        assertFalse(armed, \"400M reserve armed with a controller whose gateway can never relay settlements\");\n    }\n\n    function testOperatorEoaPublisherStillArms() public {\n        vm.prank(FOUNDER);\n        RobinhoodJobController controller = new RobinhoodJobController(OPERATOR);\n        bool armed = _deployFundArm(controller);\n        assertFalse(armed, \"400M reserve armed with operator EOA as immutable controller publisher\");\n    }\n}","reproduction":"Case A: as FOUNDER on chainid 4663 deploy gateway = new PublisherGateway(operator); controller = new RobinhoodJobController(gateway); SKIP gateway.bindController; deploy FounderFundedMining(canonical config with authority=controller); controller.bindMiner(miner); FINCR.transfer(miner, 400M); miner.arm(). Expected: arm (or construction) reverts because the publisher gateway is not bound. Actual: arm succeeds; afterwards gateway.bindController(controller, codehash) reverts InvalidConfiguration (miner()!=0), so no settlement can ever be relayed. Case B: controller = new RobinhoodJobController(0x174818c4FEcB65472522Ff8C7677F5A567cB3020) (operator EOA); same steps; arm succeeds. `forge test --match-path test/scratch/UnboundGatewayArm.t.sol` fails both tests with the stated messages.","severity":"medium","snippet":"        if (address(jobs.miner()) != address(this)) revert InvalidCanonicalConfiguration();","title":"arm() commits the irreversible 400M reserve without verifying that the controller's immutable publisher is a gateway bound to that controller (misordered binding or operator-EOA publisher strands the "},{"citation":"resolved","description":"Reviewed commit 0c866371fb280cf91df04ad2c5a85a412d998246. FounderFundedMining's 'InvalidCanonicalConfiguration' guard pins chain, FINCR codehash, supply, 10/0.5 IMD quote/floor and maxMarketBatch, but leaves c.operations (only required to be a code-less address != initializer), c.initializer (any msg.sender) and c.roundSeconds/c.grace/c.maxAge (anything within AttestedAgentRounds' wide 1h-30d / <=30d / <=1d bounds) free. All are immutable, the 400M transfer is irreversible and there is no rescue. TRUST_AND_LIMITATIONS.md promises 24-hour rounds, up to one day grace and one-hour identity freshness, and LAUNCH.md designates Operations 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e; a constructor typo is only caught by off-chain review. PublisherGateway already hard-codes FOUNDER/OPERATIONS, so pinning them here is consistent. Precondition: founder deployment error; impact: 3.75% of every bid auto-paid (payOperationsExact) to a wrong immutable address, or permanently non-canonical round cadence/grace for the whole 365-day agent stream.","line":37,"path":"src/FounderFundedMining.sol","reproduction":"On chainid 4663, as the deployer, construct FounderFundedMining with the otherwise-canonical config used in test/Task30Gateway.t.sol but operations = 0x000000000000000000000000000000000000dEaD (EOA, no code) and roundSeconds = 30 days, grace = 30 days, maxAge = 1 days. Expected: InvalidCanonicalConfiguration (non-published values). Actual: construction, controller.bindMiner, funding and arm all succeed; every later takeover's operations bucket is pushed to 0x...dEaD and agent periods are 30 days with 30-day grace.","severity":"low","snippet":"            block.chainid != 4663 || c.initializer != msg.sender || c.initializer == c.operations","title":"Canonical constructor pins economics but not the published Operations recipient, founder initializer or 24h/1-day/1-hour round timing"}],"hash":"7de38345cc013856df60a40ac8c71f242e77410f92b87ce8cdf26277c1dd2631","nodeId":"8ae674c0-7725-4a73-a2ec-3a3d9a13b9f2","outcome":"completed","summary":"I found no critical or high defects in my area (Priority 4, end-to-end deployment and operation) at commit `0c866371fb280cf91df04ad2c5a85a412d998246`. I found two medium and one low. My recommendation for this area is **FIX FIRST**; the final verdict is the judge's once all four areas are merged. The checked-out commit matched the requested one, so this is not BLOCKED. The three findings are in `.imd-findings.json`, and the two mediums each include a Foundry test that I ran and saw fail on this code. Apart from that file, I left the repository unchanged.\n\n## Findings\n\n1. **Medium: whoever is the current financier can wipe out a whole period's 20% agent payout.** `src/RobinhoodJobController.sol:141`\n   - **How:** a job can be bought up to the last second of a period (`commission`/`dispatchWork`), but it can only be bound before the period ends (`AttestedAgentRounds.sol:240`). Settlement requires every purchased job to be bound.\n   - **Effect:** after a last-second purchase, `finalizeRound` reverts with `InvalidSettlement` permanently. The only remaining step is `expire()`, which locks the whole period's agent budget away for good, even when every job succeeded and agents had already been accepted. In the first era that is about 2M FINCR per day.\n   - **Who can do it:** anyone who bids, down to the 0.5 IMD floor. The 0.5 IMD job fee comes from the shared work budget, not the attacker's own funds.\n   - **Proof:** the test fails on this code. When I simulated a fix that refuses the late purchase, it passes.\n   - This is separate from the documented case where the off-chain resolver refuses a period with mixed failed and successful jobs. Here, every job succeeds and there is still no on-chain way to settle.\n\n2. **Medium: `arm()` locks in the 400M without checking that the publisher is a gateway bound to this controller.** `src/FounderFundedMining.sol:63`\n   - **Case A, wrong order:** if `controller.bindMiner` runs before `gateway.bindController`, the gateway can never bind afterwards. Funding and `arm()` still succeed.\n   - **Case B, wrong publisher:** a controller deployed with the operator's own wallet (`0x1748…3020`) as publisher passes every on-chain check, even though the launch policy requires the separate gateway.\n   - **Effect:** in Case A the 80M FINCR agent stream can never be settled. In Case B the gateway's pause and operator-rotation controls are lost. Both tests fail on this code.\n\n3. **Low: the canonical constructor doesn't pin the Operations recipient, the founder as initializer, or the documented round timing (24-hour rounds, 1-day grace, 1-hour identity window).** `src/FounderFundedMining.sol:37`\n   - A deployment typo here becomes permanent: for example, the 3.75% Operations share of every bid would go to the wrong address forever.\n\n## Checks run\n| Check | Result |\n|---|---|\n| `forge build` + `script/verify-build.py` | Passed: 10 builds, 19 ABIs, 471 manifest files. I used Foundry 1.8.3 rather than the documented 1.7.1, and the pins still matched. |\n| `forge test` | 257 passed, 0 failed |\n| Extended invariants (`FOUNDRY_PROFILE=extended forge test --match-test invariant`) | 4 test suites passed: IntegrationInvariant, MiningInvariant (Floor and Zero), Task15Invariant. **The Task16 and Task23 invariant suites didn't finish** within my 7-minute cap. |\n| Fork rehearsal `rehearse-real-factory.py --match-test testTask33` | Passed on a local, keyless Robinhood fork at block 93365633. Nothing was broadcast. |\n| Real factory receipt, actual deployed runtime, live 50/10/40 allocation | **Not checked.** Nothing is deployed yet. |\n\n## Coverage of my area\n| Entry point | Status and reason |\n|---|---|\n| `PublisherGateway` constructor, `setPaused`, `replaceOperator` | Holds. Founder-only on chain 4663, starts paused, rotating the operator re-pauses it, and an operator can't be a contract or a protected address. |\n| `PublisherGateway.bindController` | Holds on its own. Binding is one-time and requires matching code hash, gateway","treeHash":null,"usage":{"cachedInputTokens":2850269,"inputTokens":72,"model":"claude-opus-5-5","outputTokens":33066,"runtime":"claude","turns":40,"wallClockMs":897457}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d73af718d55ba788","findings":[{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. FinancierMiningWithSettlement.dispatchWork (src/FinancierMiningWithSettlement.sol:122) accepts a purchase while `block.timestamp < c.end`, but AttestedAgentRounds.bindJob (src/AttestedAgentRounds.sol:240) also requires `block.timestamp < c.end` and `a.admittedAt <= block.timestamp`, so a request purchased at c.end-1 can only be bound in that same second, which the off-chain admission flow cannot meet. RobinhoodJobController.finalizeRound then reverts InvalidSettlement forever because `requestRoundPlusOne(purchased[i]) != round + 1`, and after graceSeconds anyone can call AttestedAgentRounds.expire, which moves the whole round budget to totalQuarantined. The caller is the current seat holder (anyone who paid the live Dutch price, which is 0.5 IMD after an hour); the 0.5 IMD work fee is paid from the protocol's work bucket, not from the attacker. Victims are every agent with accepted work in that round: in era 1 the per-day agent allocation is about 1.96M FINCR, and an incumbent repeating this daily zeroes the entire 20% agent stream (80M FINCR) while keeping its own 80% tenure stream. The same on-chain rule also bricks a round in honest operation whenever any of the up to three purchases is refused or not admitted by the backend before c.end, because an unadmitted request has no job to bind. Suggested fix: stop accepting dispatches late in the window (e.g. require block.timestamp + a binding margin < c.end), allow binding during the grace period, or let finalizeRound tolerate a purchased request that has a recorded failure or was never admitted instead of refusing the whole round.","line":141,"path":"src/RobinhoodJobController.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {AttestedAgentRounds as Rounds} from \"src/AttestedAgentRounds.sol\";\nimport {IAgentWorkVerifier as W} from \"src/interfaces/IAgentWorkVerifier.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ninterface IIntakeControl {\n    function setPrice(bytes32, address, uint256) external;\n    function setPayTo(address) external;\n    function setWriter(address) external;\n    function setCallbackGas(uint64) external;\n    function complete(bytes32, uint8, bytes32, string calldata, bytes calldata) external;\n}\n\n/// Finding: the current financier can dispatch work at `c.end - 1` (FinancierMiningWithSettlement.dispatchWork\n/// allows `block.timestamp < c.end`), but AttestedAgentRounds.bindJob requires `block.timestamp < c.end` too and\n/// `admittedAt <= block.timestamp`, so a request purchased in the last second can never be bound. RobinhoodJobController\n/// .finalizeRound then reverts forever for that round (`requestRoundPlusOne(purchased[i]) != round + 1`), and the whole\n/// agent allocation, including already accepted work, expires to quarantine. Cost to the financier: 0.5 IMD of the\n/// protocol's own work budget. Victims: every agent with accepted work in that round.\ncontract LateCommissionQuarantineTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant MANAGER = 0x8366a39CC670B4001A1121B8F6A443A643e40951;\n    address constant GUARD = 0x19Bec7C2E1b2aadAf67b259744751a9960d66000;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant OPERATIONS = 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e;\n    address constant OPERATOR = address(700);\n    address constant FINANCIER = address(101);\n    address constant AGENT_OWNER = address(301);\n    bytes32 constant JOB = keccak256(\"SYNTHETIC FINANCIER job UUID\");\n\n    FINCR token;\n    FounderFundedMining miner;\n    RobinhoodJobController controller;\n    PublisherGateway gateway;\n    Rounds rounds;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(MANAGER, vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\")));\n        vm.etch(GUARD, vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\")));\n        vm.store(IMD, 0, bytes32(uint256(uint160(address(this)))));\n        (bool ok,) = IMD.call(abi.encodeWithSignature(\"enableTransfers()\"));\n        require(ok);\n        vm.store(INTAKE, 0, bytes32(uint256(uint160(address(this)))));\n        IIntakeControl(INTAKE).setPrice(\"job.open@job-1\", IMD, 0.5 ether);\n        IIntakeControl(INTAKE).setPayTo(address(800));\n        IIntakeControl(INTAKE).setWriter(address(this));\n        IIntakeControl(INTAKE).setCallbackGas(200_000);\n\n        token = new FINCR(1e27);\n        vm.prank(FOUNDER);\n        gateway = new PublisherGateway(OPERATOR);\n        vm.prank(FOUNDER);\n        controller = new RobinhoodJobController(address(gateway));\n        vm.prank(FOUNDER);\n        gateway.bindController(controller, address(controller).codehash);\n        vm.prank(FOUNDER);\n        gateway.setPaused(false);\n        FinancierMiningWithMarket.Config memory c = FinancierMiningWithMarket.Config(\n            address(token),\n            IMD,\n            1e27,\n            10 ether,\n            0.5 ether,\n            FOUNDER,\n            OPERATIONS,\n            address(controller),\n            controller.SPECIFICATION(),\n            1 days,\n            1 days,\n            1 hours,\n            0.1 ether\n        );\n        vm.prank(FOUNDER);\n        miner = new FounderFundedMining(c);\n        vm.prank(FOUNDER);\n        controller.bindMiner(INativeJobMining(address(miner)));\n        rounds = miner.agentRounds();\n        token.transfer(address(miner), 4e26);\n        vm.prank(FOUNDER);\n        miner.arm();\n        deal(IMD, FINANCIER, 100 ether);\n        vm.prank(FINANCIER);\n        IERC20(IMD).approve(address(miner), type(uint256).max);\n    }\n\n    function testLateCommissionCannotBlockAcceptedWorkSettlement() public {\n        // Genesis bid: FINANCIER holds the seat and funds the work bucket (5% of 10 IMD = 0.5 IMD).\n        vm.prank(FINANCIER);\n        miner.bid(0, 10 ether, block.timestamp);\n        W.Context memory c = rounds.context(0);\n\n        // Honest job: commissioned, admitted and bound inside the window, then completed successfully.\n        vm.prank(FINANCIER);\n        bytes32 honest = controller.commission(0, 0);\n        bytes32 bodyHash = keccak256(controller.body(0, 0));\n        vm.prank(OPERATOR);\n        gateway.bindJob(\n            0, Rounds.Admission(honest, JOB, bodyHash, block.timestamp, block.timestamp, keccak256(\"admission\"))\n        );\n        uint256 boundAt = block.timestamp;\n        bytes32 result = keccak256(\"SYNTHETIC accepted FINANCIER report\");\n        IIntakeControl(INTAKE).complete(honest, 0, result, \"uri\", abi.encode(honest, result, \"uri\"));\n\n        // Griefing step: the same financier buys a second scoped body in the LAST second of the window.\n        // (Low-level call so this test also passes if the fix is to reject late dispatches.)\n        vm.prank(FINANCIER);\n        miner.fundWork(0.5 ether); // keep the work bucket funded regardless of bid split\n        vm.warp(c.end - 1);\n        vm.prank(FINANCIER);\n        (bool dispatched,) = address(controller).call(abi.encodeCall(controller.commission, (0, 1)));\n        dispatched; // ignored on purpose\n\n        // Window closed: the late request can no longer be bound (bindJob requires block.timestamp < c.end).\n        vm.warp(c.end);\n        Rounds.Snapshot memory s = Rounds.Snapshot(\n            26_000_000, keccak256(\"finalized ETH header\"), c.end - 15 minutes, keccak256(\"ownerOf\"), keccak256(\"set\")\n        );\n        vm.prank(OPERATOR);\n        gateway.lockSnapshot(0, s);\n\n        W.Contribution[] memory a = new W.Contribution[](1);\n        Rounds.OwnerEvidence[] memory o = new Rounds.OwnerEvidence[](1);\n        a[0] = W.Contribution(\n            1,\n            rounds.IDENTITY_REGISTRY(),\n            51_000,\n            AGENT_OWNER,\n            AGENT_OWNER,\n            JOB,\n            keccak256(\"receipt\"),\n            keccak256(\"submission\"),\n            keccak256(\"artifact\"),\n            boundAt + 2,\n            s.timestamp,\n            c.deadline\n        );\n        o[0] = Rounds.OwnerEvidence(100, false, 0, 0, boundAt + 1);\n        bytes32 commitment = rounds.settlementCommitment(0, a, s, o);\n\n        // Expected: the honestly bound, completed job settles and the agent owner can claim its share.\n        // Actual on this commit: RobinhoodJobController.finalizeRound reverts InvalidSettlement because the late\n        // purchased request has requestRoundPlusOne == 0, and after the grace period the budget is quarantined.\n        vm.prank(OPERATOR);\n        gateway.finalizeRound(0, a, s, o, commitment);\n        uint256 budget = miner.agentAllocation(0, 1 days);\n        assertEq(rounds.claimable(0, AGENT_OWNER), budget);\n        vm.prank(AGENT_OWNER);\n        assertEq(rounds.claim(0), budget);\n        assertEq(token.balanceOf(AGENT_OWNER), budget);\n    }\n}","reproduction":"Deploy gateway, controller, FounderFundedMining on chain 4663 with the pinned IMD/Intake/PoolManager runtimes, fund 400M FINCR and arm. ALICE bids at epoch 0 (10 IMD). ALICE commissions (0,0); operator binds it; Intake completes it with status 0. ALICE warps to context(0).end - 1 and calls controller.commission(0,1): succeeds (workRequests recorded, 0.5 IMD spent). At context(0).end the operator locks the snapshot and calls finalizeRound with the accepted agent of job (0,0). Expected: round finalizes, claimable(0, owner) == agentAllocation(0, 1 days), owner claims it. Actual: finalizeRound reverts InvalidSettlement(); after deadline expire(0) quarantines the full budget and claim reverts NoClaim. Control: the identical sequence without the late commission finalizes and pays (test/scratch/Control.t.sol passed).","severity":"high","snippet":"        for (uint256 i; i < purchased.length; ++i) {\n            (bool recorded,,,) = miner.completions(purchased[i]);\n            if (!recorded || miner.agentRounds().requestRoundPlusOne(purchased[i]) != round + 1) {\n                revert InvalidSettlement();\n            }\n        }","title":"Current financier can permanently quarantine a round's agent allocation by purchasing work in the last second of the window (dispatch allowed until c.end, binding required before c.end, finalization r"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. onFailure reverts unless status is 1 or 2, and onResult (src/FounderFundedMining.sol:97) reverts for resultHash == 0 or a URI longer than 2048 bytes. Against the pinned Robinhood Intake runtime (test/fixtures/task16/intake-runtime.hex) a reverting callback does not revert the writer's `complete` call: the request is marked `completed` and a second `complete` for the same id is rejected (verified in test/scratch: complete(status=3) ok=true, intake completed=true, miner recorded=false, retry ok=false; same for a 2049-byte URI). RobinhoodJobController.finalizeRound (src/RobinhoodJobController.sol:140-148) requires `recorded` for every purchased request and every bound job, so one such closure makes the round unfinalizable and its agent allocation expires to quarantine; accepted agents in the same round lose their share. This is not attacker-triggered (the writer is the trusted Intake backend) but it is a plausible honest integration mismatch: the status set and URI length are backend-defined, and the same silent-swallow also applies if the production Intake's callbackGas is below what _record needs (about 90k: three cold SSTOREs, an external `requests` view, EXTCODEHASH and an event carrying the URI), which must be verified on the live Intake storage before funding. Suggested fix: make _record total for any caller == WORK_INTAKE and known request: record every status value, store keccak of the URI without a length bound (or truncate), and accept a zero result hash as a recorded failure; keep the strict acceptance checks in finalizeRound where status 0 and non-zero result are already required.","line":102,"path":"src/FounderFundedMining.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {IIMDIntakeViews} from \"src/interfaces/IIMDIntakeViews.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ninterface IIntakeControl {\n    function setPrice(bytes32, address, uint256) external;\n    function setPayTo(address) external;\n    function setWriter(address) external;\n    function setCallbackGas(uint64) external;\n    function complete(bytes32, uint8, bytes32, string calldata, bytes calldata) external;\n}\n\n/// Finding: FounderFundedMining.onFailure reverts for any status other than 1 or 2 and onResult reverts for a\n/// zero result hash or a URI longer than 2048 bytes. The pinned Intake runtime (test/fixtures/task16) swallows the\n/// reverted callback, marks the request `completed`, and rejects any later `complete` for the same id. The miner\n/// therefore never records the completion, RobinhoodJobController.finalizeRound requires `recorded` for every\n/// purchased request and bound job, and the round's agent allocation can only expire into quarantine.\ncontract UnrecordableCompletionTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant MANAGER = 0x8366a39CC670B4001A1121B8F6A443A643e40951;\n    address constant GUARD = 0x19Bec7C2E1b2aadAf67b259744751a9960d66000;\n    address constant ALICE = address(101);\n    FINCR token;\n    FounderFundedMining miner;\n    RobinhoodJobController controller;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(MANAGER, vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\")));\n        vm.etch(GUARD, vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\")));\n        vm.store(IMD, 0, bytes32(uint256(uint160(address(this)))));\n        (bool ok,) = IMD.call(abi.encodeWithSignature(\"enableTransfers()\"));\n        require(ok);\n        vm.store(INTAKE, 0, bytes32(uint256(uint160(address(this)))));\n        IIntakeControl(INTAKE).setPrice(\"job.open@job-1\", IMD, 0.5 ether);\n        IIntakeControl(INTAKE).setPayTo(address(800));\n        IIntakeControl(INTAKE).setWriter(address(this));\n        IIntakeControl(INTAKE).setCallbackGas(200_000);\n        token = new FINCR(1e27);\n        controller = new RobinhoodJobController(address(this));\n        miner = new FounderFundedMining(\n            FinancierMiningWithMarket.Config(\n                address(token),\n                IMD,\n                1e27,\n                10 ether,\n                0.5 ether,\n                address(this),\n                address(105),\n                address(controller),\n                controller.SPECIFICATION(),\n                1 days,\n                1 days,\n                1 hours,\n                0.1 ether\n            )\n        );\n        controller.bindMiner(INativeJobMining(address(miner)));\n        deal(IMD, ALICE, 100 ether);\n        vm.prank(ALICE);\n        IERC20(IMD).approve(address(miner), type(uint256).max);\n        token.transfer(address(miner), 4e26);\n        miner.arm();\n        vm.prank(ALICE);\n        miner.bid(0, 10 ether, block.timestamp);\n    }\n\n    /// The Intake writer closes the request with failure status 3 (e.g. timeout). The miner's onFailure reverts,\n    /// the Intake keeps `completed == true`, no retry is possible, and the miner never records the closure.\n    function testIntakeFailureStatusOutsideOneOrTwoIsStillRecorded() public {\n        vm.prank(ALICE);\n        bytes32 id = controller.commission(0, 0);\n        IIntakeControl(INTAKE).complete(\n            id, 3, bytes32(\"timeout\"), \"\", abi.encode(id, uint8(3), bytes32(\"timeout\"), uint16(0), uint16(0), bytes(\"\"))\n        );\n        (,,, bool completed) = IIMDIntakeViews(INTAKE).requests(id);\n        assertTrue(completed, \"intake closed the request\");\n        // A second writer call for the same id is rejected by the Intake runtime: there is no retry path.\n        (bool retried,) = INTAKE.call(\n            abi.encodeCall(\n                IIntakeControl.complete,\n                (id, 2, 0, \"\", abi.encode(id, uint8(2), bytes32(\"failed\"), uint16(0), uint16(0), bytes(\"\")))\n            )\n        );\n        assertFalse(retried, \"intake refuses a second completion\");\n        // Expected: the miner recorded the closure so the round can still be finalized or evaluated.\n        // Actual on this commit: completions[id].recorded stays false forever.\n        (bool recorded,,,) = miner.completions(id);\n        assertTrue(recorded, \"miner must record every closure the Intake delivers\");\n    }\n}","reproduction":"Same deployment as above; ALICE bids and commissions (0,0). Writer calls Intake.complete(id, 3, 'timeout', '', abi.encode(id, uint8(3), 'timeout', 0, 0, '')). Expected: miner.completions(id).recorded == true (status 3 stored) so finalizeRound can still run (and reject the job as not status 0). Actual: Intake.requests(id).completed == true, a retry complete(id, 2, ...) reverts, miner.completions(id).recorded == false forever; finalizeRound for round 0 reverts InvalidSettlement at the purchased-request loop and only expire() remains.","severity":"medium","snippet":"    function onFailure(bytes32 requestId, uint8 status, bytes32 reason, uint16, uint16, bytes calldata) external {\n        if (status != 1 && status != 2) revert InvalidCompletion();\n        _record(requestId, status, reason, \"\");","title":"Miner callbacks reject well-formed Intake closures (failure status other than 1/2, zero result hash, URI > 2048 bytes); the pinned Intake swallows the revert and forbids retries, so the completion is "},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. executeMarket is the only spender of buybackBudget and liquidityBudget, and it only runs as a gas-capped self-call during bid. PermanentV4Market.makeOrder/_validate read the pool's slot0 via extsload; for a pool key (IMD, FINCR, fee 12500, spacing 60, hooks GUARD) that was never initialized spot() is 0 and every attempt reverts PriceBounds, which FinancierMiningWithMarket._attemptMarket (src/FinancierMiningWithMarket.sol:94-102) turns into a MarketDeferred event. There is no founder, operator or time-based alternative path, so 7.5% + 3.75% of all bid volume stays in the vault forever if the official pool is not created exactly as pinned, or stays below MIN_BATCH forever. TRUST_AND_LIMITATIONS.md says optional buyback/LP operations can fail, but does not state that the funds then have no exit. Suggested fix: document the dependency explicitly in the launch checklist (pool initialized with the exact key before arm) or add a bounded fallback such as letting the buckets roll into workBudget after a long deferral.","line":118,"path":"src/market/PermanentV4Market.sol","reproduction":"Deploy FounderFundedMining with the pinned PoolManager runtime but without initializing the pool; arm; ALICE bids 10 IMD. Expected per design: 0.75 IMD buyback and 0.375 IMD liquidity are executed or remain claimable later. Actual: bid emits MarketDeferred(false, 0.75e18, PriceBounds) and MarketDeferred(true, 0.375e18, PriceBounds); buybackBudget and liquidityBudget stay >0 with no function able to move them; imdLiability includes them permanently.","severity":"low","snippet":"        uint160 fair = spot();\n        if (fair <= SQRT_LOWER || fair >= SQRT_UPPER) revert PriceBounds();","title":"Buyback and liquidity buckets (11.25% of every bid) have no exit other than the pinned v4 pool; if that pool is never initialized or its spot leaves the usable band, the IMD accumulates in the vault p"}],"hash":"982df5255212e0c53551cb57c4073a72cb8214870b653ffc1806254ed1670360","nodeId":"f4cc3691-da6e-49fb-81b9-0441e6412c53","outcome":"completed","summary":"Review complete for commit 0c866371fb280cf91df04ad2c5a85a412d998246 (checked-out HEAD matches; origin is imd-financier/financier-open). Findings are in `.imd-findings.json` with two Foundry proofs under `test/scratch/`. No tracked file was changed.\n\n**Findings**\n\n- **High. Late or unadmitted work purchase bricks a round's agent settlement.** `dispatchWork` accepts a purchase until the last second of the window, but `bindJob` also needs to land before `c.end`, and `finalizeRound` refuses the whole round while any purchased request is unbound. The current seat holder (anyone who paid the live Dutch price) can commission at `c.end - 1` using the protocol's own work bucket, after which the round can only expire into quarantine. Agents with already accepted work lose their share, and a persistent incumbent can zero the entire 20% agent stream. The same rule bricks a round in honest operation when the backend refuses or delays admission of any purchase. Proof `test/scratch/LateCommissionQuarantine.t.sol` fails with `InvalidSettlement`; the identical sequence without the late purchase passes.\n- **Medium. Miner callbacks reject well-formed Intake closures and the Intake never retries.** `onFailure` reverts for any status other than 1 or 2, and `onResult` reverts on a zero hash or a URI over 2048 bytes. Against the pinned Intake runtime, a reverting callback still marks the request completed and a second `complete` is refused, so the completion is unrecordable and the round is unfinalizable. The same silent swallow applies if the live Intake's callback gas is below what `_record` needs, which must be verified before funding. Proof `test/scratch/UnrecordableCompletion.t.sol` fails on this commit.\n- **Low. Buyback and liquidity buckets have no exit except the pinned v4 pool.** With the pool uninitialized, every bid emits `MarketDeferred` with `PriceBounds` for both buckets, and 11.25% of bid volume accumulates in the vault with no spending path. Verified by probe.\n\n**Coverage of the assigned area**\n\n| Entry point | Status |\n|---|---|\n| FINCR constructor, burn | holds |\n| arm, bid, claimFincr, claimImd, claimOperations, payAgentReward | holds (CEI, shared guard, exact-transfer checks, liability and custody arithmetic traced) |\n| payOutgoingExact, payOperationsExact, executeMarket self-calls | holds (self-only, gas-capped, failures roll back budgets) |\n| fundWork, dispatchWork, _purchaseWork | finding 1 (window), otherwise holds |\n| onResult, onFailure, _record | finding 2 |\n| EmissionSchedule gross/split | holds (continuous, monotone, telescoping agent budgets) |\n| AttestedAgentRounds finalize, expire, claim, SeatOwner overrides | holds (no double claim or replay found) |\n| PermanentV4Market execute, unlockCallback, _add, _settle | holds within pinned runtime; finding 3 on stranded buckets |\n| RobinhoodJobController, PublisherGateway | finding 1; other areas belong to the gateway reviewer |\n\n**Checks run:** forge build, full suite excluding scratch (257 passed, 0 failed, including invariants at default profile), three scratch tests. Not run: extended profile, Slither or Mythril (not available), any live-chain or fork verification of Intake callback gas, pool initialization, or FINCR runtime hash on Robinhood.\n\n**Leads outside my area, not reported as findings:** the incumbent can self-bid at the 0.5 IMD floor each hour for a net 0.1 IMD to reset the opening quote to 10 IMD (auction economics); IMD transfers are owner-enableable in the pinned runtime, so a pause freezes bids and claims (trust assumption); accounts with EIP-7702 delegation code are quarantined by the code-length policy.","treeHash":null,"usage":{"cachedInputTokens":3037206,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":50115,"runtime":"claude","turns":50,"wallClockMs":877080}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"232b62e021f6f394","findings":[{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced (merged from audit_specialist, audit_specialist_2, audit_specialist_3 and audit_specialist_4; same root cause). RobinhoodJobController.finalizeRound (lines 138-144) refuses to finalize unless EVERY request purchased in the round is both completion-recorded and bound via bindJob (requestRoundPlusOne == round+1). commission -> FinancierMiningWithSettlement.dispatchWork (src/FinancierMiningWithSettlement.sol:122) accepts a purchase at any block.timestamp < c.end, while AttestedAgentRounds.bindJob (src/AttestedAgentRounds.sol:240) also requires block.timestamp < c.end and needs the backend admission first (admittedAt <= block.timestamp). A request purchased close enough to c.end that it cannot be admitted and bound before c.end can therefore never be bound, finalizeRound reverts InvalidSettlement for the rest of the grace period regardless of how many other jobs were bound, completed and accepted, and after c.deadline anyone calls AttestedAgentRounds.expire, which moves the whole round budget to totalQuarantined (never redirectable). Preconditions: caller is the current financier (anyone who paid the live Dutch price, 0.5 IMD at the floor, 80% of which is refunded on the next takeover) and the shared work bucket holds 0.5 IMD (anyone can top it up with fundWork). The 0.5 IMD job fee comes from the protocol's work bucket, not from the attacker. Impact: in era 1 one 24h round carries agentAllocation(0, 1 days) = 1,959,183.67 FINCR; an incumbent repeating this each day zeroes the full 20% agent stream (80M FINCR) while keeping its own 80% tenure stream. The same on-chain rule also bricks a round in honest operation whenever a commissioned request is refused or not admitted by the backend before c.end, or when the founder pauses the gateway (e.g. operator rotation always pauses) between a commission and c.end so the operator cannot bind. TRUST_AND_LIMITATIONS.md only discloses that failed jobs block the off-chain resolver; this is an on-chain veto with every job succeeded. Suggested fix (finding only): close commission/dispatchWork a binding margin before c.end, allow bindJob during the grace period for requests dispatched inside the window, or let finalizeRound tolerate a purchased request that was never bound by window end (it cannot carry accepted agents because permittedJob requires bindJob).","line":141,"path":"src/RobinhoodJobController.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {AttestedAgentRounds as Rounds} from \"src/AttestedAgentRounds.sol\";\nimport {IAgentWorkVerifier as W} from \"src/interfaces/IAgentWorkVerifier.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ninterface IIntakeControl {\n    function setPrice(bytes32, address, uint256) external;\n    function setPayTo(address) external;\n    function setWriter(address) external;\n    function setCallbackGas(uint64) external;\n    function complete(bytes32, uint8, bytes32, string calldata, bytes calldata) external;\n}\n\n/// Finding: the current financier can dispatch work at `c.end - 1` (FinancierMiningWithSettlement.dispatchWork\n/// allows `block.timestamp < c.end`), but AttestedAgentRounds.bindJob requires `block.timestamp < c.end` too and\n/// `admittedAt <= block.timestamp`, so a request purchased in the last second can never be bound. RobinhoodJobController\n/// .finalizeRound then reverts forever for that round (`requestRoundPlusOne(purchased[i]) != round + 1`), and the whole\n/// agent allocation, including already accepted work, expires to quarantine. Cost to the financier: 0.5 IMD of the\n/// protocol's own work budget. Victims: every agent with accepted work in that round.\ncontract LateCommissionQuarantineTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant MANAGER = 0x8366a39CC670B4001A1121B8F6A443A643e40951;\n    address constant GUARD = 0x19Bec7C2E1b2aadAf67b259744751a9960d66000;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant OPERATIONS = 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e;\n    address constant OPERATOR = address(700);\n    address constant FINANCIER = address(101);\n    address constant AGENT_OWNER = address(301);\n    bytes32 constant JOB = keccak256(\"SYNTHETIC FINANCIER job UUID\");\n\n    FINCR token;\n    FounderFundedMining miner;\n    RobinhoodJobController controller;\n    PublisherGateway gateway;\n    Rounds rounds;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(MANAGER, vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\")));\n        vm.etch(GUARD, vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\")));\n        vm.store(IMD, 0, bytes32(uint256(uint160(address(this)))));\n        (bool ok,) = IMD.call(abi.encodeWithSignature(\"enableTransfers()\"));\n        require(ok);\n        vm.store(INTAKE, 0, bytes32(uint256(uint160(address(this)))));\n        IIntakeControl(INTAKE).setPrice(\"job.open@job-1\", IMD, 0.5 ether);\n        IIntakeControl(INTAKE).setPayTo(address(800));\n        IIntakeControl(INTAKE).setWriter(address(this));\n        IIntakeControl(INTAKE).setCallbackGas(200_000);\n\n        token = new FINCR(1e27);\n        vm.prank(FOUNDER);\n        gateway = new PublisherGateway(OPERATOR);\n        vm.prank(FOUNDER);\n        controller = new RobinhoodJobController(address(gateway));\n        vm.prank(FOUNDER);\n        gateway.bindController(controller, address(controller).codehash);\n        vm.prank(FOUNDER);\n        gateway.setPaused(false);\n        FinancierMiningWithMarket.Config memory c = FinancierMiningWithMarket.Config(\n            address(token),\n            IMD,\n            1e27,\n            10 ether,\n            0.5 ether,\n            FOUNDER,\n            OPERATIONS,\n            address(controller),\n            controller.SPECIFICATION(),\n            1 days,\n            1 days,\n            1 hours,\n            0.1 ether\n        );\n        vm.prank(FOUNDER);\n        miner = new FounderFundedMining(c);\n        vm.prank(FOUNDER);\n        controller.bindMiner(INativeJobMining(address(miner)));\n        rounds = miner.agentRounds();\n        token.transfer(address(miner), 4e26);\n        vm.prank(FOUNDER);\n        miner.arm();\n        deal(IMD, FINANCIER, 100 ether);\n        vm.prank(FINANCIER);\n        IERC20(IMD).approve(address(miner), type(uint256).max);\n    }\n\n    function testLateCommissionCannotBlockAcceptedWorkSettlement() public {\n        // Genesis bid: FINANCIER holds the seat and funds the work bucket (5% of 10 IMD = 0.5 IMD).\n        vm.prank(FINANCIER);\n        miner.bid(0, 10 ether, block.timestamp);\n        W.Context memory c = rounds.context(0);\n\n        // Honest job: commissioned, admitted and bound inside the window, then completed successfully.\n        vm.prank(FINANCIER);\n        bytes32 honest = controller.commission(0, 0);\n        bytes32 bodyHash = keccak256(controller.body(0, 0));\n        vm.prank(OPERATOR);\n        gateway.bindJob(\n            0, Rounds.Admission(honest, JOB, bodyHash, block.timestamp, block.timestamp, keccak256(\"admission\"))\n        );\n        uint256 boundAt = block.timestamp;\n        bytes32 result = keccak256(\"SYNTHETIC accepted FINANCIER report\");\n        IIntakeControl(INTAKE).complete(honest, 0, result, \"uri\", abi.encode(honest, result, \"uri\"));\n\n        // Griefing step: the same financier buys a second scoped body in the LAST second of the window.\n        // (Low-level call so this test also passes if the fix is to reject late dispatches.)\n        vm.prank(FINANCIER);\n        miner.fundWork(0.5 ether); // keep the work bucket funded regardless of bid split\n        vm.warp(c.end - 1);\n        vm.prank(FINANCIER);\n        (bool dispatched,) = address(controller).call(abi.encodeCall(controller.commission, (0, 1)));\n        dispatched; // ignored on purpose\n\n        // Window closed: the late request can no longer be bound (bindJob requires block.timestamp < c.end).\n        vm.warp(c.end);\n        Rounds.Snapshot memory s = Rounds.Snapshot(\n            26_000_000, keccak256(\"finalized ETH header\"), c.end - 15 minutes, keccak256(\"ownerOf\"), keccak256(\"set\")\n        );\n        vm.prank(OPERATOR);\n        gateway.lockSnapshot(0, s);\n\n        W.Contribution[] memory a = new W.Contribution[](1);\n        Rounds.OwnerEvidence[] memory o = new Rounds.OwnerEvidence[](1);\n        a[0] = W.Contribution(\n            1,\n            rounds.IDENTITY_REGISTRY(),\n            51_000,\n            AGENT_OWNER,\n            AGENT_OWNER,\n            JOB,\n            keccak256(\"receipt\"),\n            keccak256(\"submission\"),\n            keccak256(\"artifact\"),\n            boundAt + 2,\n            s.timestamp,\n            c.deadline\n        );\n        o[0] = Rounds.OwnerEvidence(100, false, 0, 0, boundAt + 1);\n        bytes32 commitment = rounds.settlementCommitment(0, a, s, o);\n\n        // Expected: the honestly bound, completed job settles and the agent owner can claim its share.\n        // Actual on this commit: RobinhoodJobController.finalizeRound reverts InvalidSettlement because the late\n        // purchased request has requestRoundPlusOne == 0, and after the grace period the budget is quarantined.\n        vm.prank(OPERATOR);\n        gateway.finalizeRound(0, a, s, o, commitment);\n        uint256 budget = miner.agentAllocation(0, 1 days);\n        assertEq(rounds.claimable(0, AGENT_OWNER), budget);\n        vm.prank(AGENT_OWNER);\n        assertEq(rounds.claim(0), budget);\n        assertEq(token.balanceOf(AGENT_OWNER), budget);\n    }\n}","reproduction":"Setup as test/Task30Gateway.t.sol (chainid 4663, pinned IMD/Intake runtimes, gateway -> controller -> bindController -> miner -> bindMiner, 400M funded, arm, roundSeconds = 1 days, grace = 1 days, maxAge = 1 hours). 1) ALICE bids 10 IMD (genesis); ALICE calls controller.commission(0,0) -> request H; operator gateway.bindJob(0, admission for H/JOB); Intake writer completes H with status 0 and non-zero hash. 2) vm.warp(context(0).end - 1); ALICE calls controller.commission(0,1) -> succeeds, 0.5 IMD leaves workBudget, second purchasedRequests entry. 3) vm.warp(context(0).end); gateway.bindJob for the second request reverts WrongTime; operator gateway.lockSnapshot(0, s) succeeds; operator gateway.finalizeRound(0, [one valid accepted contribution for JOB], s, owners, settlementCommitment). Expected: round 0 finalizes with count 1 and OWNER claims agentAllocation(0, 1 days) = 1959183673469387755102041 wei FINCR. Actual: finalizeRound reverts InvalidSettlement() at src/RobinhoodJobController.sol:141-143 because requestRoundPlusOne(second request) == 0; after context(0).deadline rounds.expire(0) succeeds and totalQuarantined == the full budget. Judge reproduction: forge test --match-path test/scratch/Proof_965f872dbf01.t.sol fails with InvalidSettlement() on this commit; the identical flow without step 2 is the passing Task30Gateway testEqualPerAgentAggregatesTwoSeatEntitlementsInOneWalletClaim, isolating the late purchase as the cause.","severity":"high","snippet":"            if (!recorded || miner.agentRounds().requestRoundPlusOne(purchased[i]) != round + 1) {","title":"Any current financier can veto a whole period's agent settlement by commissioning work in the last seconds of the window; the period's 20% agent budget is quarantined"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced (merged from audit_specialist, audit_specialist_2 and audit_specialist_3). FounderFundedMining.onResult reverts InvalidCompletion for resultHash == 0, an empty URI or a URI longer than 2048 bytes (line 97), and onFailure reverts for any status other than 1 or 2 (line 102). Against the pinned Robinhood Intake runtime (test/fixtures/task16/intake-runtime.hex, codehash pinned in src/FinancierMiningWithSettlement.sol:18) a reverting payer callback does not revert the writer's complete(): the request is marked completed, a second complete() for the same id is rejected, and completions[id].recorded stays false forever. RobinhoodJobController.finalizeRound (lines 139-149) requires recorded == true for every purchased and every bound request, so the round can never be finalized and its entire agent budget (about 1.96M FINCR per day in era 1) is quarantined on expire, including shares earned on other accepted jobs. Precondition: a benign backend output shape (long result URI, unusual failure code, zero hash) from the trusted Intake, or a production callbackGas below what _record needs; not attacker-triggered, but self-inflicted by the callback and not disclosed. Whether an agent can influence the result URI length through artifact naming cannot be determined from this tree. Suggested fix (finding only): make the callback total for msg.sender == WORK_INTAKE and a known request (store the raw status and keccak of the URI without length/zero checks); keep acceptance gated in finalizeRound, which already requires status == 0 and result != 0.","line":97,"path":"src/FounderFundedMining.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {AttestedAgentRounds as Rounds} from \"src/AttestedAgentRounds.sol\";\nimport {IIMDIntakeViews} from \"src/interfaces/IIMDIntakeViews.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ninterface ISeatIntake {\n    function setPrice(bytes32, address, uint256) external;\n    function setPayTo(address) external;\n    function setWriter(address) external;\n    function setCallbackGas(uint64) external;\n    function complete(bytes32, uint8, bytes32, string calldata, bytes calldata) external;\n}\n\n/// The pinned Intake runtime marks a request completed even when the payer callback reverts and\n/// refuses a second completion. FounderFundedMining.onResult reverts for any uri longer than 2048\n/// bytes (and for a zero resultHash; onFailure reverts for any status other than 1/2), so one such\n/// official result leaves `completions[request].recorded == false` forever. finalizeRound then\n/// reverts for the whole period and its agent budget is quarantined on expiry. Fails on this code;\n/// passes once the callback records every official completion instead of reverting.\ncontract RevertingCallbackStrandsRoundTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant ALICE = address(101);\n    address constant ATTESTER = address(700);\n    bytes32 constant JOB = keccak256(\"SYNTHETIC FINANCIER job UUID\");\n    PublisherGateway gateway;\n    FINCR token;\n    FounderFundedMining miner;\n    RobinhoodJobController controller;\n    Rounds rounds;\n    bytes32 request;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(0x8366a39CC670B4001A1121B8F6A443A643e40951, vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\")));\n        vm.etch(0x19Bec7C2E1b2aadAf67b259744751a9960d66000, vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\")));\n        vm.store(IMD, 0, bytes32(uint256(uint160(address(this)))));\n        (bool ok,) = IMD.call(abi.encodeWithSignature(\"enableTransfers()\"));\n        require(ok);\n        vm.store(INTAKE, 0, bytes32(uint256(uint160(address(this)))));\n        ISeatIntake(INTAKE).setPrice(\"job.open@job-1\", IMD, 0.5 ether);\n        ISeatIntake(INTAKE).setPayTo(address(800));\n        ISeatIntake(INTAKE).setWriter(address(this));\n        ISeatIntake(INTAKE).setCallbackGas(200_000);\n        token = new FINCR(1e27);\n        vm.prank(FOUNDER);\n        gateway = new PublisherGateway(ATTESTER);\n        vm.prank(FOUNDER);\n        controller = new RobinhoodJobController(address(gateway));\n        vm.prank(FOUNDER);\n        gateway.bindController(controller, address(controller).codehash);\n        vm.prank(FOUNDER);\n        gateway.setPaused(false);\n        FinancierMiningWithMarket.Config memory c = FinancierMiningWithMarket.Config(\n            address(token), IMD, 1e27, 10 ether, 0.5 ether, FOUNDER, 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e,\n            address(controller), controller.SPECIFICATION(), 1 days, 1 days, 1 hours, 0.1 ether\n        );\n        vm.prank(FOUNDER);\n        miner = new FounderFundedMining(c);\n        vm.prank(FOUNDER);\n        controller.bindMiner(INativeJobMining(address(miner)));\n        rounds = miner.agentRounds();\n        deal(IMD, ALICE, 100 ether);\n        vm.prank(ALICE);\n        IERC20(IMD).approve(address(miner), type(uint256).max);\n        token.transfer(address(miner), 4e26);\n        vm.prank(FOUNDER);\n        miner.arm();\n        vm.prank(ALICE);\n        miner.bid(0, 10 ether, block.timestamp);\n        vm.prank(ALICE);\n        request = controller.commission(0, 0);\n        bytes32 bodyHash = keccak256(controller.body(0, 0));\n        vm.prank(ATTESTER);\n        gateway.bindJob(0, Rounds.Admission(request, JOB, bodyHash, block.timestamp, block.timestamp, keccak256(\"admission\")));\n        vm.warp(block.timestamp + 10);\n    }\n\n    function testOfficialResultWithLongUriMustStillBeRecorded() public {\n        bytes memory longUri = new bytes(2049);\n        for (uint256 i; i < longUri.length; ++i) longUri[i] = \"a\";\n        bytes32 hash = keccak256(\"SYNTHETIC nonempty completed result\");\n        // Official writer completes the request once; the pinned runtime does not revert when the\n        // payer callback reverts, and it refuses any second completion of the same request.\n        ISeatIntake(INTAKE).complete(request, 0, hash, string(longUri), abi.encode(request, hash, string(longUri)));\n        (,,, bool completedAtIntake) = IIMDIntakeViews(INTAKE).requests(request);\n        assertTrue(completedAtIntake, \"intake marked complete\");\n        vm.expectRevert();\n        ISeatIntake(INTAKE).complete(request, 0, hash, \"uri\", abi.encode(request, hash, \"uri\"));\n        (bool recorded,,,) = miner.completions(request);\n        assertTrue(recorded, \"official completion must be recorded so the period can settle\");\n    }\n}","reproduction":"Setup as test/Task30Gateway.t.sol; ALICE bids and calls commission(0,0) -> request R; operator binds R to JOB. Writer calls Intake.complete(R, 0, keccak256('result'), <2049-byte URI>, abi.encode(R, hash, uri)). Expected: miner.completions(R).recorded == true (or the completion is retryable) so round 0 can still settle. Actual: complete() succeeds, IIMDIntakeViews(INTAKE).requests(R).completed == true, miner.completions(R).recorded == false, a retry complete(R, 0, hash, 'uri', ...) reverts; finalizeRound(0, ...) reverts InvalidSettlement at src/RobinhoodJobController.sol:141 forever and after context(0).deadline expire(0) quarantines 1959183673469387755102041 wei FINCR. Same outcome with complete(R, 3, 'timeout', '', abi.encode(R, uint8(3), 'timeout', 0, 0, '')) via onFailure. Judge reproduction: forge test --match-path test/scratch/Proof_4acf8e628448.t.sol fails with 'official completion must be recorded so the period can settle' on this commit.","severity":"medium","snippet":"        if (resultHash == 0 || bytes(uri).length == 0 || bytes(uri).length > 2048) revert InvalidCompletion();","title":"Official Intake completion that the miner callback rejects (URI > 2048 bytes, empty URI, zero result hash, failure status outside {1,2}) is never recorded and permanently blocks the period"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced (merged from audit_specialist_2 findings dc17dfda and 66147ba0 and audit_specialist_4 finding fb563aa2; same root cause). LAUNCH.md step 4 requires gateway -> controller -> gateway.bindController BEFORE miner binding. On-chain the order is enforced in one direction only: PublisherGateway.bindController (src/PublisherGateway.sol:50) rejects a controller whose miner() is already set, but RobinhoodJobController.bindMiner (lines 47-52) and FounderFundedMining.arm (line 63) never check PublisherGateway(publisher).controller() == address(this), and the FounderFundedMining constructor (line 43) only rejects jobs.publisher() == c.operations, so an EOA publisher (including the operator EOA 0x174818c4FEcB65472522Ff8C7677F5A567cB3020, forbidden by policy) also passes. Case A (miner bound first): bindController reverts InvalidConfiguration forever, the controller's immutable publisher (the gateway) can never call bindJob/lockSnapshot/finalizeRound, yet bindMiner, the 400M transfer and arm all succeed; every agent round can only expire(), so 80,000,000 FINCR (the 20% stream) is permanently quarantined in a miner with no rescue path. Case B (operator EOA publisher): construction, bindMiner and arm succeed; the operator holds unpausable, unrotatable publisher power for the full year and the gateway can never be inserted. Precondition: a founder deployment-order or constructor-argument error; impact is permanent and all other launch invariants (epoch()==0, miner()==0, codehash, policyHash) are already enforced on-chain at the same gates. Suggested fix (finding only): in bindMiner or arm require jobs.publisher().code.length != 0 and PublisherGateway(jobs.publisher()).controller() == address(jobs) (optionally FOUNDER() == initializer).","line":63,"path":"src/FounderFundedMining.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.30;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PublisherGateway} from \"src/PublisherGateway.sol\";\nimport {FINCR} from \"src/FINCR.sol\";\nimport {FounderFundedMining} from \"src/FounderFundedMining.sol\";\nimport {RobinhoodJobController, INativeJobMining} from \"src/RobinhoodJobController.sol\";\nimport {FinancierMiningWithMarket} from \"src/FinancierMiningWithMarket.sol\";\nimport {AttestedAgentRounds as Rounds} from \"src/AttestedAgentRounds.sol\";\n\n/// Deployment order is only enforced one way: PublisherGateway.bindController rejects a controller\n/// that already has a miner, but RobinhoodJobController.bindMiner and FounderFundedMining.arm never\n/// check that the immutable publisher (the gateway) has actually been bound to this controller.\n/// If the founder binds the miner first, the gateway can never bind, no publisher call can ever\n/// reach the controller, every agent round expires to quarantine, and 80M FINCR (20% of the\n/// 400M reserve) is stranded in the miner with no recovery path. Fails on this code; passes once\n/// bindMiner (or arm) refuses while the gateway is not bound to this controller.\ncontract MisorderedBindingStrandsAgentsTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant INTAKE = 0xa43e6F75ee006411F79Ac1C84120606C2330DE82;\n    address constant FOUNDER = 0xbcF19c0821A245383c127570615896Abac9e424F;\n    address constant OPERATOR = 0x174818c4FEcB65472522Ff8C7677F5A567cB3020;\n\n    function testMinerCannotArmWhileGatewayIsUnboundToController() public {\n        vm.chainId(4663);\n        vm.warp(1_800_000_000);\n        vm.etch(IMD, vm.parseBytes(vm.readLine(\"test/fixtures/task16/imd-runtime.hex\")));\n        vm.etch(INTAKE, vm.parseBytes(vm.readLine(\"test/fixtures/task16/intake-runtime.hex\")));\n        vm.etch(0x8366a39CC670B4001A1121B8F6A443A643e40951, vm.parseBytes(vm.readLine(\"test/fixtures/task23/manager-runtime.hex\")));\n        vm.etch(0x19Bec7C2E1b2aadAf67b259744751a9960d66000, vm.parseBytes(vm.readLine(\"test/fixtures/task23/guard-runtime.hex\")));\n        FINCR token = new FINCR(1e27);\n        vm.prank(FOUNDER);\n        PublisherGateway gateway = new PublisherGateway(OPERATOR);\n        vm.prank(FOUNDER);\n        RobinhoodJobController controller = new RobinhoodJobController(address(gateway));\n        FinancierMiningWithMarket.Config memory c = FinancierMiningWithMarket.Config(\n            address(token), IMD, 1e27, 10 ether, 0.5 ether, FOUNDER, 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e,\n            address(controller), controller.SPECIFICATION(), 1 days, 1 days, 1 hours, 0.1 ether\n        );\n        vm.prank(FOUNDER);\n        FounderFundedMining miner = new FounderFundedMining(c);\n\n        // Wrong order: miner bound before the gateway binding.\n        vm.prank(FOUNDER);\n        try controller.bindMiner(INativeJobMining(address(miner))) {} catch {}\n\n        // Gateway binding is now impossible forever (bindController requires candidate.miner() == 0).\n        vm.prank(FOUNDER);\n        try gateway.bindController(controller, address(controller).codehash) {} catch {}\n\n        token.transfer(address(miner), 4e26);\n        vm.prank(FOUNDER);\n        try miner.arm() {} catch {}\n\n        // Nobody can ever call the controller as publisher: the gateway holds no controller,\n        // and the controller's immutable publisher is the gateway, not an EOA.\n        bool publisherReachable = address(gateway.controller()) == address(controller);\n        // Defect: the reserve is armed while the agent settlement path is unreachable.\n        assertFalse(miner.armed() && !publisherReachable, \"armed with permanently unreachable publisher\");\n    }\n}","reproduction":"Case A: chainid 4663, pinned runtimes; vm.prank(FOUNDER): gateway = new PublisherGateway(operator); controller = new RobinhoodJobController(gateway); miner = new FounderFundedMining(canonical config with authority = controller); controller.bindMiner(miner) -> succeeds; gateway.bindController(controller, controller.codehash) -> reverts InvalidConfiguration (miner() != 0); transfer 400M FINCR; miner.arm() -> succeeds. Expected: bindMiner or arm refuses while gateway.controller() != controller. Actual: armed() == true with gateway.controller() == address(0) and no publisher able to reach the controller. Case B: controller = new RobinhoodJobController(0x174818c4FEcB65472522Ff8C7677F5A567cB3020); same steps; construction, bindMiner, funding and arm all succeed (test/Task27DSeatOwner.t.sol already binds an EOA attester as publisher the same way). Judge reproduction: forge test --match-path test/scratch/Proof_dc17dfda5158.t.sol fails with 'armed with permanently unreachable publisher' on this commit.","severity":"medium","snippet":"        if (address(jobs.miner()) != address(this)) revert InvalidCanonicalConfiguration();","title":"bindMiner and arm never verify that the controller's immutable publisher is a gateway bound to this controller; miner-first ordering or an operator-EOA publisher strands the 80M FINCR agent stream aft"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced by code trace (merged from audit_specialist_2 cf31b5d1 and audit_specialist_3 c68dbc82). FounderFundedMining.arm only calls jobs.prepareRound(0) (line 64). precommitRound refuses a scope once the round has started (line 210), and nothing in bid(), commission(), bindJob() or finalize() prepares later rounds. For an unprepared round, permittedRequestBody is false so dispatchWork reverts InvalidWorkIntent, bindJob and finalize revert ScopeUnavailable, and the only terminal state is expire() -> full quarantine (1,959,183.67 FINCR per day in era 1). Rated low rather than medium because the mitigation needs no trust: anyone may call controller.prepareRound(id) for all 365 rounds immediately after arm (before genesis the time check is skipped entirely, and any future round is allowed afterwards), so one batch of founder transactions at launch removes the daily keeper dependency. No automation is supplied or claimed, and neither LAUNCH.md nor TRUST_AND_LIMITATIONS.md lists this batch as a launch step. Suggested fix (finding only): precommit all rounds in arm() or lazily in bid()/dispatchWork, or allow precommit for a round whose window has started but not ended (bodies are deterministic), and add the batch call to LAUNCH.md.","line":210,"path":"src/AttestedAgentRounds.sol","reproduction":"State: armed miner, genesis bid at t0, nobody calls prepareRound(1) before t0 + 1 days. At block.timestamp = context(1).start: controller.prepareRound(1) reverts WrongTime (line 210); rounds.scopes(1).committed == false; financier commission(1,0) reverts InvalidWorkIntent in dispatchWork. At context(1).deadline + 1: rounds.expire(1) succeeds and totalQuarantined grows by agentAllocation(1 days, 2 days) == 1959183673469387755102041 wei. Expected: the day's agent emissions remain settleable or roll forward; actual: permanently quarantined.","severity":"low","snippet":"        if (vault.epoch() != 0 && block.timestamp >= vault.genesis() + id * roundSeconds) revert WrongTime();","title":"Only round 0 is prepared on-chain; a missed prepareRound(id) before a round starts quarantines that day's agent budget"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced by code trace (audit_specialist_2 930dd1b8). lockSnapshot only requires block.timestamp >= c.end and <= c.deadline (line 115), finalizeRound carries no minimum separation from SnapshotLocked, and AttestedAgentRounds.finalize (line 299) accepts block.timestamp == c.end. A compromised operator can lock a fabricated snapshot, finalize an invented accepted set naming itself as controller/beneficiary and claim in the same block at c.end. The founder pause/rotation powers that PublisherGateway v2 presents as the mitigation for operator-key compromise therefore have no observation window. This is inside the disclosed trust boundary (the operator can misallocate the bounded 20% stream), so it is a mitigation gap, not a new loss class. Suggested fix (finding only): require finalizeRound at least N seconds after SnapshotLocked for the round, bounded by the grace period.","line":115,"path":"src/RobinhoodJobController.sol","reproduction":"Setup as test/Task30Gateway.t.sol; complete the bound job; vm.warp(context(0).end); vm.startPrank(ATTESTER): gateway.lockSnapshot(0, s); gateway.finalizeRound(0, [contribution with controller = beneficiary = ATTESTER-controlled EOA], s, owners, commitment); rounds.claim(0) from that EOA returns 1959183673469387755102041 wei FINCR in the same block. Expected per the gateway design: a founder review window between lock and settlement. Actual: none.","severity":"low","snippet":"            snapshotDigest[round] != 0 || block.timestamp < c.end || block.timestamp > c.deadline || s.blockNumber == 0","title":"No minimum delay between lockSnapshot and finalizeRound: founder pause cannot intercept a compromised operator's same-block settlement"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced by code trace (audit_specialist_3 2065234d). bid() (lines 139-163) does not distinguish msg.sender == iFinancier and _allocate credits 80% of the price back to the outgoing financier (lines 174-181), paid out automatically by FinancierMiningWithSettlement in the same transaction. A self-bid at live price P therefore costs the incumbent 0.2P net while resetting the quote to 10 IMD, whereas a challenger must pay P. Resetting whenever the price hits the 0.5 IMD floor holds the seat for 0.1 IMD per hour (about 877 IMD per year) for the entire 80% financier stream (320M FINCR), and a same-block self-bid makes any observed challenger's bid revert StaleEpoch (line 141). Using a second wallet gives the same 0.2P cost, so blocking self-bids alone does not help. Existing tests (AutoOutgoing testSelfBidSpendsOnlyNetWaterfallButDoesNotInflateEmissions, Auction testSameBlockSelfTakeoversNoEmissionsAndPartialRecovery) show the authors know self-bids cost 20% net; TOKENOMICS.md does not disclose that defense is five times cheaper than attack. No protocol funds are lost; the price-discovery guarantee implied by the auction is not delivered. Treated as a disclosed-adjacent economic limitation, reported low for the record.","line":180,"path":"src/FinancierMining.sol","reproduction":"State: armed, ALICE genesis bid, vm.warp(+1 hours) so currentPrice() == 0.5 IMD. BOB submits bid(1, 0.5e18, deadline); in the same block ALICE's bid(1, 0.5e18, deadline) lands first: ALICE pays 0.5 IMD, receives 0.4 IMD back via payOutgoingExact (net 0.1 IMD), epoch becomes 2, resetPrice = 10 IMD; BOB's bid reverts StaleEpoch. Expected per TOKENOMICS: price discovery between financiers; actual: the incumbent pays 0.2P to keep the slot against any challenger paying P.","severity":"low","snippet":"            outgoingCredit[previous] += outgoing;","title":"Incumbent financier can defend the seat at one fifth of a challenger's price (self-takeover refunds 80% to itself), so the Dutch auction is effectively first-bidder-wins"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced by code trace (audit_specialist_3 f5c7b769). EmissionSchedule.gross returns the full reserve for elapsed >= 365 days, so no financier accrual exists after genesis + 365 days, but bid() has no emission-end check and currentPrice() keeps decaying from 10 IMD after every takeover. A post-end bidder pays the live quote: 20% is routed to work/buyback/LP/operations and 80% to the previous financier (whose cursor _checkpoint already set to the full 320M), and the bidder's claimableFincr is 0 forever (f - financierCursor == 0). Suggested fix (finding only): revert bids once elapsed() >= EMISSION_END or surface it in currentPrice().","line":139,"path":"src/FinancierMining.sol","reproduction":"State: armed, ALICE genesis bid. vm.warp(genesis + 365 days + 1 hours). New EOA with 1 IMD calls bid(1, 0.5e18, block.timestamp). Actual: 0.5 IMD debited (0.4 IMD to ALICE via payOutgoingExact, 0.1 IMD into buckets), epoch == 2; after 30 more days claimableFincr(newEOA) == 0 and currentTenureEarned() == 0. Expected: a bid that can never earn emissions is refused.","severity":"low","snippet":"    function bid(uint256 expectedEpoch, uint256 maxPrice, uint256 deadline) external nonReentrant {","title":"bid() accepts payments after EMISSION_END: a late bidder pays the live quote and can never earn any FINCR"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced by code trace (audit_specialist b27797d4). FinancierMiningWithMarket.executeMarket is the only spender of buybackBudget and liquidityBudget and runs only as a gas-capped self-call during bid (_attemptMarket, lines 94-102). PermanentV4Market.makeOrder/_validate read slot0 via extsload for the fixed key (IMD, FINCR, fee 12500, spacing 60, hooks GUARD); for a never-initialized pool spot() == 0 so line 118 reverts PriceBounds, which the vault turns into a MarketDeferred event. There is no founder, operator or time-based alternative path, so 7.5% + 3.75% of all bid volume stays in the vault (and in imdLiability) permanently if the official pool is not created with exactly that key before bids, or if the bucket stays below MIN_BATCH. TRUST_AND_LIMITATIONS.md says optional buyback/LP operations can fail but not that the funds then have no exit. Suggested fix (finding only): add 'pool initialized with the exact key before arm' to LAUNCH.md, or a bounded fallback such as rolling long-deferred buckets into workBudget.","line":118,"path":"src/market/PermanentV4Market.sol","reproduction":"Deploy FounderFundedMining with the pinned PoolManager runtime but without initializing the pool; arm; ALICE bids 10 IMD. Expected per design: 0.75 IMD buyback and 0.375 IMD liquidity are executed or remain claimable later. Actual: bid emits MarketDeferred(false, 0.75e18, PriceBounds) and MarketDeferred(true, 0.375e18, PriceBounds); buybackBudget and liquidityBudget stay > 0 with no function able to move them.","severity":"low","snippet":"        if (fair <= SQRT_LOWER || fair >= SQRT_UPPER) revert PriceBounds();","title":"Buyback and liquidity buckets (11.25% of every bid) have no exit other than the pinned v4 pool; an uninitialized or out-of-band pool leaves that IMD in the vault forever"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Reproduced by code trace (audit_specialist_4 6e12e5a8). The InvalidCanonicalConfiguration guard (lines 36-45) pins chain, FINCR codehash, supply, 10/0.5 IMD quote/floor and maxMarketBatch, but c.operations only has to be a code-less address different from the initializer, and c.roundSeconds/c.grace/c.maxAge may be anything inside AttestedAgentRounds' 1h-30d / <=30d / <=1d bounds. All are immutable, the 400M transfer is irreversible and there is no rescue. TRUST_AND_LIMITATIONS.md promises 24-hour rounds, up to one day grace and one-hour identity freshness, and LAUNCH.md designates Operations 0x74421cdb9e6ad159358c4af7C59CFAE472A0541e; PublisherGateway already hard-codes FOUNDER/OPERATIONS. Precondition: founder constructor-argument error caught only by off-chain review. Impact: 3.75% of every bid auto-paid (payOperationsExact) to a wrong immutable address, or non-canonical cadence/grace for the whole 365-day agent stream.","line":37,"path":"src/FounderFundedMining.sol","reproduction":"On chainid 4663, as FOUNDER, construct FounderFundedMining with the config of test/Task30Gateway.t.sol but operations = 0x000000000000000000000000000000000000dEaD and roundSeconds = 30 days, grace = 30 days, maxAge = 1 days. Expected: InvalidCanonicalConfiguration. Actual: construction, controller.bindMiner, 400M funding and arm all succeed; every takeover pushes the operations bucket to 0x...dEaD and agent periods are 30 days with 30-day grace.","severity":"low","snippet":"            block.chainid != 4663 || c.initializer != msg.sender || c.initializer == c.operations","title":"Canonical constructor pins economics but not the published Operations recipient or the 24h/1-day/1-hour round timing"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Coverage note (audit_specialist_2 6b7f4cb6), verified: PublisherGateway is referenced by exactly one test file (test/Task30Gateway.t.sol) and FounderFundedMining only by Task27BGenesis, Task27DSeatOwner and Task30Gateway; test/IntegrationRehearsal.t.sol and test/IntegrationInvariant.t.sol (via test/helpers/IntegrationBase.sol) deploy the historical ReserveForwarder with mocks, and Task27DSeatOwner binds an EOA attester as publisher. The documented production topology (gateway as immutable publisher, paused start, one-time binding, operator forwarding) is covered by a single unit suite and by no invariant run or deployment rehearsal. No on-chain defect by itself.","line":66,"path":"test/Task30Gateway.t.sol","reproduction":"grep -l PublisherGateway test/*.sol test/helpers/*.sol -> only test/Task30Gateway.t.sol; grep -l FounderFundedMining test/*.sol -> Task27BGenesis, Task27DSeatOwner, Task30Gateway; grep -n 'new ' test/helpers/IntegrationBase.sol -> ReserveForwarder + mocks. Expected: the rehearsal reproduces LAUNCH.md steps 4-5; actual: it rehearses the legacy forwarder.","severity":"info","snippet":"        gateway = new PublisherGateway(ATTESTER);","title":"Only test/Task30Gateway.t.sol exercises the production topology; the integration rehearsal and invariant suites bind the legacy forwarder or an EOA publisher"},{"citation":"resolved","description":"Commit 0c866371fb280cf91df04ad2c5a85a412d998246. Verdict for commit 0c866371fb280cf91df04ad2c5a85a412d998246: FIX FIRST (not NO-GO: no unauthorized FINCR/IMD withdrawal, double claim, reentrancy or reserve-stranding path was found in the custody/accounting layer; the blocking defects are liveness vetoes on the 20% agent stream and missing deployment gates). Prioritized blockers before any 400M transfer or arm: (1) high, late-commission veto of agent settlement (src/RobinhoodJobController.sol:141); (2) medium, callback-rejected official completions permanently unrecorded (src/FounderFundedMining.sol:97); (3) medium, arm/bindMiner without gateway-bound publisher check (src/FounderFundedMining.sol:63). Recommended before launch: batch prepareRound for all rounds, pool initialization in LAUNCH.md, constructor pinning of operations/timing. Dropped or merged: the four late-commission reports and the three callback reports are one finding each; the two gateway-binding reports and the operator-EOA-publisher report are one finding. No specialist assertion was dropped as unreproducible; severities were recalibrated by impact (financier self-bid asymmetry, post-end bids, market-bucket exit and timing pins are low). Checks performed by the judge: HEAD verified == the pinned SHA; forge build (solc 0.8.30, optimizer 200, bytecode_hash none) succeeds; forge test excluding test/scratch: 34 suites, 257 tests passed, 0 failed, 0 skipped (default profile: fuzz 512, invariant 128x128 fail_on_revert); forge build --sizes: FounderFundedMining runtime 18,784 B, initcode 46,047 B (under the 24,576/49,152 limits; founder-deployed per LAUNCH.md, not factory CREATE2), PermanentV4Market 10,909 B, SeatOwnerAgentRounds 11,763 B, RobinhoodJobController 8,196 B, PublisherGateway 3,667 B, FINCR 1,897 B; all four specialist proofs fail on this commit for their stated reasons. Not run: any RPC/cast read of Robinhood 4663 or Ethereum (no deployed addresses exist), the extended profile, slither/mythril, and an end-to-end deployment rehearsal of LAUNCH.md steps 4-6 against live runtimes (the tree only rehearses them with vm.etch'd pinned runtimes). Trust assumptions kept as documented: trusted publisher/operator for the 20% stream, founder pause/rotation, immutable economics; no invented guarantees, addresses or launch success.","line":5,"path":"LAUNCH.md","reproduction":"Re-run: git rev-parse HEAD == 0c866371fb280cf91df04ad2c5a85a412d998246; forge build; forge test --no-match-path 'test/scratch/*' (expect 257 passed); forge test --match-path 'test/scratch/Proof_*' (expect all 4 fail on this commit).","severity":"info","snippet":"1. Review this exact public audit candidate, commission one separately approved independent security job, reproduce findings and resolve critical/high issues. Publication is not an audit.","title":"Judge verdict: FIX FIRST. Checks run on this commit: forge build, full forge test (257 passed, 0 failed, incl. invariants), contract sizes, four specialist proofs; not run: live-chain reads, deploymen"}],"hash":"a8e1a99b13c01297677e444fb99f9bfda555c28423e7814e1861cf8737ebc3d9","nodeId":"ae4bb320-a91e-4db3-8d0d-da1de3148657","outcome":"completed","summary":"All eleven snippets match the tree and the findings file is valid JSON.\n\n**Verdict for commit 0c866371fb280cf91df04ad2c5a85a412d998246: FIX FIRST.** The findings are in `.imd-findings.json` at the repository root (11 entries: 1 high, 2 medium, 6 low, 2 info).\n\n**What I verified**\n- HEAD equals the pinned SHA, working tree clean.\n- Build succeeds, and the full documented suite passes with 257 tests across 34 suites including the invariant runs. Scratch tests excluded.\n- Contract sizes are under both EVM limits. The mining contracts are founder-deployed per LAUNCH.md, not factory CREATE2, so the factory deployment constraint does not apply to them.\n- All four specialist proofs fail on this commit for exactly the reason each finding states.\n\n**Blockers, in priority order**\n1. **High.** Any current financier can commission work in the last seconds of a period. That request can never be bound before the window closes, so `finalizeRound` reverts forever and the whole period's agent budget (about 1.96M FINCR per day in era 1) is quarantined. Costs the attacker nothing beyond holding the seat. Four specialists reported this; merged into one finding with the strongest proof attached.\n2. **Medium.** Official Intake completions the miner callback rejects (long URI, empty URI, zero hash, unusual failure code) are marked complete at the Intake but never recorded in the miner, which permanently blocks that period. Three reports merged.\n3. **Medium.** `bindMiner` and `arm` never check that the controller's publisher is a gateway bound to that controller. Binding the miner first, or using the operator EOA as publisher, passes every on-chain gate and strands the 80M FINCR agent stream after the irreversible funding. Three reports merged.\n\n**Kept as low** after recalibration: missed daily `prepareRound` (anyone can batch-prepare all rounds after arm, which should be a launch step), no lock-to-finalize delay for founder intervention, self-bid refund asymmetry, bids accepted after emission end, buyback and liquidity buckets with no exit if the pool is never initialized, and the constructor not pinning operations or round timing. No specialist claim failed to reproduce; nothing was dropped.\n\n**Not run, disclosed in the file:** live-chain reads (no deployed addresses exist), the extended fuzz profile, Slither or Mythril, and a deployment rehearsal against real runtimes rather than the pinned etched fixtures. No repository files were changed other than creating the findings file and the throwaway test/scratch copies.","treeHash":null,"usage":{"cachedInputTokens":816186,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":27205,"runtime":"claude","turns":26,"wallClockMs":475427}}],"verification":[]}