{"jobId":"7f2023fc-fa1f-4a7f-a507-3bbc712c51d6","repoUrl":"https://github.com/OoJae/help-wanted.git","baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","count":10,"submissions":[{"hash":"8c44bd4796267e3457be505504b00b445f6be8d7ed7e75842eae9437e278974b","nodeKey":"audit_judge","role":"review","attempt":1,"deviceKey":"918f8261a6fd589cb41cfa8a8105d9b1d139eed39376ccdc56150d01a5b0f39d","seat":{"tokenId":"1430","agentId":"52114"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":30,"runtime":"claude","inputTokens":386,"wallClockMs":743354,"outputTokens":34922,"cachedInputTokens":1558153},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"76ec6a10359004db747e8b3cb36c4d4cefa84c8f","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2","a8d8414d8d7d3421a9dfd7509df974995cf1c6e652834f197c88168a29ae9f21","49b7dcc4e6e4b727b5abed0bb98b5a20e06f936096d89aee738c543d3c5ccbaf"],"verificationErrors":0,"summary":"The review is complete and `.imd-findings.json` holds five findings and a 21-row coverage record.\n\n**Outcome.** No critical, high or medium defect. All five specialist findings reproduce on this tree, and I kept each at the severity its impact supports. Nothing blocks the launch.\n\n**What I verified**\n- The project builds with the pinned settings and all 441 tests pass. Runtime sizes are 13,161 (Endowments), 13,762 (Rules) and 3,834 (Zap) bytes.\n- The manifest's argument order matches every constructor. All six mainnet addresses have code, sIMD's asset is IMD and its owner is zero, WorkOrders reports IMD, and the undeclared live functions Zap relies on (`fundFor`, `chipInFor`) are present in the deployed bytecode. The v4 pool with Zap's hard-wired key is initialized with liquidity.\n- One scratch test file (`test/scratch/Specialists.t.sol`) reproduces four specialist findings against the real code. The fifth is covered by an existing repository test.\n\n**Findings kept, by severity**\n1. **Low.** `createForPot` accepts a pot that already needs nothing, so a \"Pot\" endowment pays all yield to its creator-chosen fallback while the on-chain record says it funds a pot. Asymmetric with `createForMeter`'s liveness check.\n2. **Low.** Endowments and Rules accept the sibling Zap as a payee, and Zap hands its whole IMD balance to whoever calls it next, so a harvester can collect the yield in the same transaction. Requires the creator to choose Zap. Only yield, never principal.\n3. **Info.** The pot branch of the harvest plan has no `MIN_HARVEST` floor. A pot one wei short makes a gas-floored harvest move one wei.\n4. **Info.** `createRule` validates unused condition fields but accepts and publishes unused action fields in the event.\n5. **Info.** The documented every-block sIMD hold grief, recorded as accepted by design.\n\n**Coverage.** All 19 listed entry points are answered: 16 hold, 3 carry a finding (`createForPot`, `createForRecipient`, `harvest`), plus a row for `createRule` and one for the manifest and dependency checks. No row is unreached.","createdAt":"2026-10-15T13:15:21.895Z","verdict":null,"findings":[{"severity":"low","title":"createForPot accepts a pot that already needs nothing, so a 'Pot' endowment is a disguised recipient endowment paying its fallback beneficiary","path":"src/endowments/Endowments.sol","line":326,"description":"Merged from audit_permissions. createForMeter refuses a feed Questions states no cost for (line 310, FeedNotLive), so a meter endowment can only be announced while its target can absorb yield. createForPot only checks that the id has been handed out; it never asks CROWD_POTS.remaining(potId). A pot that is already full, launched, expired or closed is accepted. For such an endowment _potRoom returns 0 on every harvest, the whole excess is offered to the pot, the pot takes nothing, and _forward (lines 942-947) pays everything to the fallback beneficiary, an address the creator chose and may be. The Created event still says kind=Pot with the pot id as target. docs/THREATS.md E6 describes a pot that fills after the endowment was announced; here the pot can be dead before anyone deposits, and the on-chain fact a depositor reads (Created.kind) says the yield funds a pot. No principal is at risk and the yield goes to a party fixed at creation, hence low. Minimal fix that keeps the design: in createForPot revert (e.g. PotNotCollecting) when CROWD_POTS.remaining(potId) == 0, mirroring createForMeter; a series always reports a non-zero remaining (mock line 20, THREATS E6) so series endowments are unaffected.","reproduction":"Reproduced on this tree with test/scratch/Specialists.t.sol (SpecialistEndowmentsRepro.test_createForPotAcceptsAFullPot, passes = defect present). State: EndowmentsBase fixture; pots.open(8, 0) so pots.remaining(8) == 0 and potCount >= 8. Call endow.createForPot(8, heir) as creator. Expected (by symmetry with createForMeter): revert. Actual: returns an id whose endowment(id).kind == Kind.Pot and beneficiary == heir. Then alice deposits 100e18, stake, roll one block, add 1% vault yield; harvestable(id) = Y > MIN_HARVEST; harvest{gas: 850_000}(id) returns Y and imd.balanceOf(heir) == Y: the whole harvest was paid to the fallback through FallbackPaid, the pot received nothing.","snippet":"        if (potId == 0 || potId > CROWD_POTS.potCount()) revert UnknownPot(potId);","citation":"resolved","blocking":false},{"severity":"low","title":"Endowments and Rules accept this launch's own Zap as a payee, and Zap hands any IMD it holds to whoever calls it next","path":"src/endowments/Endowments.sol","line":626,"description":"Merged from audit_flow. _strands refuses the six addresses Endowments knows would strand or misdirect IMD sent by plain transfer, but it cannot know the sibling Zap, since no contract of the launch refers to another. Zap._returnEverything (src/rules/Zap.sol:270-271: `uint256 imdLeft = IMD.balanceOf(address(this)); if (imdLeft != 0) IMD.safeTransfer(msg.sender, imdLeft);`) sends Zap's whole IMD balance to whoever calls buy, chipIn or fundMeter next, by design (Z4). So with Zap as a Recipient endowment's recipient, or as a Pot endowment's fallbackBeneficiary, the yield is not lost (which the NatSpec accepts for an arbitrary contract) but goes to a third party chosen by timing, and since harvest is open to anyone, the party that triggers the payout can collect it in the same transaction. The same holds for a Rules PAY rule whose pay.recipient is Zap (Rules.sol:397 refuses only zero and Rules itself). This is pattern 1/3 of docs/THREATS.md section 10 reached through a sibling contract. Only the creator's choice of target makes it reachable, and only yield (or the PAY amount), never principal, is exposed, hence low. Fixes that keep the design: have Zap pay out only what the current call produced (imdOut minus what the pot or meter took) so IMD that arrives by plain transfer stays put, or list Zap's deployed address beside the refused targets in the docs and pages.","reproduction":"Reproduced on this tree with test/scratch/Specialists.t.sol (SpecialistEndowmentsRepro.test_zapAsRecipientPaysTheNextZapCaller, passes = defect present). Deploy Zap(imd, MockPoolManager, pots, questions) next to the EndowmentsBase fixture. 1) creator calls endow.createForRecipient(address(zap)): succeeds because _strands(zap) is false. 2) alice deposits 1_000_000e18, stake, roll one block, add 1% vault yield so harvestable(id) = Y > MIN_HARVEST. 3) mallory, in one transaction, calls endow.harvest(id) (Y IMD is safeTransferred to Zap, imd.balanceOf(zap) == Y) and then zap.buy{value: 0.001 ether}(0, type(uint256).max). Expected: the yield reaches the intended target or createForRecipient refuses an address that passes IMD on to arbitrary callers. Actual: imd.balanceOf(mallory) == bought + Y and imd.balanceOf(zap) == 0: mallory received the endowment's whole harvest.","snippet":"        return to == address(0) || to == address(this) || to == address(IMD) || to == address(SIMD)\n            || to == address(QUESTIONS) || to == address(CROWD_POTS);","citation":"resolved","blocking":false},{"severity":"info","title":"Pot harvest has no MIN_HARVEST floor on the pot's room: a pot one wei short makes harvest burn shares and spend a gas-floored call to move one wei","path":"src/endowments/Endowments.sol","line":756,"description":"Merged from audit_math. The meter branch of _harvestPlan refuses a harvest whose cut amount is below MIN_HARVEST (line 739: `uint256 part = room < MIN_HARVEST ? 0 : SIMD.previewWithdraw(room);`), so a nearly full meter does not trigger a vault withdrawal for dust. The pot branch has no such cut: any non-zero room below worth becomes the harvest amount. harvestable reports the same value, so a keeper acting on harvestable(id) > 0 must send a gas limit above POT_GAS_FLOOR (about 628k; 850k per the NatSpec) and pays for a vault withdrawal, the pot call and the Harvested/Settled events to deliver one wei. Nothing is lost: the shares burned are exactly previewWithdraw(1) and come out of yield, and the next harvest pays the fallback once the pot is full. The doc comment on MIN_HARVEST ('The least IMD a harvest is run for') does not hold on the pot path. Keeper-cost and consistency note only.","reproduction":"Reproduced on this tree with test/scratch/Specialists.t.sol (SpecialistEndowmentsRepro.test_potRoomOfOneWeiIsHarvested, passes = behaviour present). EndowmentsBase fixture: pot POT needs 1000e18. alice deposits 100e18 into potId, stake, roll one block, add 1% vault yield. stranger chips 1000e18 - 1 directly into the pot so pots.remaining(POT) == 1. Then endow.harvestable(potId) == 1 and endow.harvest{gas: 850_000}(potId) succeeds and returns 1; the pot's remaining becomes 0, the fallback (heir) receives 0, and the endowment's shares drop by 124330 (= previewWithdraw(1)); 266,549 gas used against the mock pot. Expected by the MIN_HARVEST comment and the meter branch: NothingToHarvest for room < 1e15. Actual: a one-wei harvest.","snippet":"            if (room != 0 && room < worth) {","citation":"resolved","blocking":false},{"severity":"info","title":"createRule refuses unused condition fields but accepts and publishes unused action fields (a PAY half on a HIRE rule, a HIRE half on a PAY rule)","path":"src/rules/Rules.sol","line":424,"description":"Merged from audit_permissions. For the condition, fields that do not apply are rejected with FieldNotUsed (line 380 for VIEW, line 383 for FEED), so a stored rule and its RuleCreated event agree. For the action, only the matching half is stored (lines 416-421) but nothing rejects the other half, and the event publishes the caller's whole RuleParams. A HIRE rule can be created with p.pay = {recipient: X, amount: Y}: getRule(id).pay is all zeros while RuleCreated.params.pay names X and Y. No funds move on the unused half (fire reads r.pay / r.hire from storage), so this only lets a creator publish misleading terms to indexers and keeper pages that decode RuleCreated instead of calling getRule. Minimal fix: in the ACT_HIRE branch revert FieldNotUsed when p.pay.recipient != 0 || p.pay.amount != 0; in the ACT_PAY branch revert FieldNotUsed when any field of p.hire is set, mirroring the condition checks.","reproduction":"Reproduced on this tree with test/scratch/Specialists.t.sol (SpecialistRulesRepro.test_hireRuleAcceptsPayFields, passes = behaviour present). RulesBase fixture (MockDesk). createRule with when = VIEW/GE 100 on the gauge, action = ACT_HIRE, hire = the base fixture's terms, pay = {recipient: 0xBEEF, amount: 5e18}, spec 'do the thing', deposit 0. Expected (by symmetry with lines 380/383): revert FieldNotUsed. Actual: succeeds; getRule(id).pay.recipient == address(0) and pay.amount == 0 while the RuleCreated event at line 424 carries p with pay.recipient == 0xBEEF and amount == 5e18.","snippet":"        emit RuleCreated(id, msg.sender, p, spec, deposit);","citation":"resolved","blocking":false},{"severity":"info","title":"Unbounded liveness grief, accepted by design: a stranger renewing sIMD's one-block hold on Endowments every block stops every harvest and every IMD withdrawal that needs the vault","path":"src/endowments/Endowments.sol","line":894,"description":"Merged from audit_economics. Every vault exit of Endowments redeems or withdraws from address(this). sIMD stamps a holder with the block in which it was minted shares, and a transfer carries a newer stamp to the receiver, so anyone who calls sIMD.deposit(1, Endowments) (about 60k gas) in a block makes sIMD pay nothing out to Endowments in that block. Repeated every block, harvest(id) and withdraw(id, amount) for any amount above the endowment's buffer revert with the vault's own error and no yield reaches any target. Principal is not at risk: exit(id, amount, minOut, true) hands over sIMD shares in any block and buffered IMD is still paid as IMD. The requester documents this in docs/THREATS.md E3 and ADAPTATION.md and tests it (test_audit_aStrangerCanHoldTheVaultEveryBlock_andOnlyTheInKindExitIsOpen). Recorded so the review shows it was weighed: the griefer gains nothing, the loss is delay of yield and the inconvenience of leaving in kind. No change asked for.","reproduction":"Existing test test/endowments/Endowments.audit.t.sol test_audit_aStrangerCanHoldTheVaultEveryBlock_andOnlyTheInKindExitIsOpen (passes on this tree). State: an endowment with staked principal and harvestable yield (deposit 100e18, stake, roll one block, add yield). Each block the stranger approves sIMD for 1 and calls sIMD.deposit(1, address(Endowments)) (the fixture's _strangerCausesHold). In that block harvest(id) reverts with sIMD's error (not HoldActive) and withdraw(id, 100e18) reverts the same way; exit(id, 100e18, 100e18, true) succeeds and transfers sIMD shares worth at least 100e18 IMD. Expected per the contract's own design: exactly this.","snippet":"            agrees = SIMD.redeem(shares, address(this), address(this)) >= assets;","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"Endowments.createForMeter(bytes32)","verdict":"holds","why":"zero feed and costOf==0 refused, SEED pulled with TransferMismatch check, Created emitted with msg.sender; anyone may call by design"},{"target":"Endowments.createForPot(uint256,address)","verdict":"finding","findingRef":1,"why":"only potCount is checked, a pot that needs nothing is accepted; fallback refused by _strands otherwise correct"},{"target":"Endowments.createForRecipient(address)","verdict":"finding","findingRef":2,"why":"refuses the six stranding addresses; accepts sibling Zap, whose balance goes to its next caller"},{"target":"Endowments.deposit(uint256,uint256)","verdict":"holds","why":"settles first, refuses a short endowment, books principal before _pull, pull checked by balance; reentrancy locked"},{"target":"Endowments.donate(uint256,uint256)","verdict":"holds","why":"adds to buffer without principal, cannot be taken back, unknown id reverts in _settle"},{"target":"Endowments.exit(uint256,uint256,uint256,bool)","verdict":"holds","why":"traced exact and proportional plans, minOut, inKind share transfer in a hold block, HoldActive for IMD, OverPrincipal cap per depositor"},{"target":"Endowments.harvest(uint256)","verdict":"finding","findingRef":3,"why":"only excess above keep leaves, hold and price mark enforced, pot call gas-floored; pot branch lacks the MIN_HARVEST cut the meter branch has"},{"target":"Endowments.stake()","verdict":"holds","why":"50-block interval, WrongVault and PriceFell checks, batch booked at previewDeposit and minted/balance verified"},{"target":"Endowments.withdraw(uint256,uint256)","verdict":"holds","why":"_exit with minOut == amount so never pays less; buffer first then exact vault withdraw checked in _leaveVault"},{"target":"Rules.cancel(uint256)","verdict":"holds","why":"creator only, unknown id fails NotCreator, escrow zeroed and totalEscrowed reduced before transfer"},{"target":"Rules.fire(uint256,bytes)","verdict":"holds","why":"status, cooldown, gas-floored condition read, escrow debited before hire/pay, tip to first firer as the brief requires, desk pull checked exactly"},{"target":"Rules.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"desk only; unknown order ignored; credits from desk books under the balance ceiling"},{"target":"Rules.recall(uint256,uint256)","verdict":"holds","why":"creator only and order must be the rule's; desk cancels, then the desk's returned figure is credited"},{"target":"Rules.reconcile(uint256,uint256)","verdict":"holds","why":"employer must be Rules, only the fresh part of returned is credited, ReturnNotReceived keeps escrows within balance, cancelled rule pays creator"},{"target":"Rules.topUp(uint256,uint96)","verdict":"holds","why":"unknown and cancelled rules refused, escrow and totalEscrowed raised before the pull"},{"target":"Rules.createRule((...),bytes,uint96)","verdict":"finding","findingRef":4,"why":"condition fields validated; unused action half accepted and published in RuleCreated"},{"target":"Zap.buy(uint256,uint256)","verdict":"holds","why":"deadline, NoEth, minImdOut; whole balances returned to caller; refund failure tolerated only for forced ETH"},{"target":"Zap.chipIn(uint256,uint256,uint256)","verdict":"holds","why":"chip in caller's name, NothingChipped on zero, allowance cleared, surplus returned; chipInFor present on live CrowdPots"},{"target":"Zap.fundMeter(bytes32,uint256,uint256)","verdict":"holds","why":"fundFor present on live Questions (selector 0x0bbc7e6e in its runtime), shares to caller, NothingFunded on zero, allowance cleared"},{"target":"Zap.unlockCallback(bytes)","verdict":"holds","why":"PoolManager only and only inside Zap's own unlock; delta signs and ethSpent<=ethIn checked; settle value and take verified"},{"target":"invariant: manifest and dependencies","verdict":"holds","why":"argument order matches each constructor; all six addresses have mainnet code, sIMD.asset()==IMD, the v4 pool with fee 10000/spacing 200 is initialized with liquidity; runtime sizes 13,161/13,762/3,834 bytes"}],"summary":{"reported":true,"total":19,"answered":19,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"fb8a2187c6ea2b26b77614487e62dfe5f8f9c63e5a9f0f9208bc269865793631","nodeKey":"audit_economics","role":"review","attempt":1,"deviceKey":"8109087d965ddf6c22e72e732f861651102ff734494b7aa459f7a20d5cdfa40b","seat":{"tokenId":"234","agentId":"53932"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":29,"runtime":"claude","inputTokens":290,"wallClockMs":1156480,"outputTokens":43839,"cachedInputTokens":1442114},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"b27fb7750fd347415d2510c687d5984aaa17c1e4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2"],"verificationErrors":0,"summary":"The review is complete and `.imd-findings.json` is in place with one info-level entry and a full coverage record.\n\n**Outcome.** No reproducible loss-of-funds, wrong-party payment, or broken-guarantee defect was found in the economic area. Rules, Endowments and Zap behave as THREATS.md describes on every entry point I traced. The only item recorded is the already-documented liveness grief on Endowments, kept at info severity because the requester has accepted it and principal remains reachable through in-kind exit.\n\n**What I checked, by guide.**\n- Economic Security: tip front-running and keeper churn in Rules (bounded by creator-set tip, cooldown, maxFires, escrow), fallback-beneficiary capture of pot harvests (creator-fixed trust assumption), meter filling around a harvest (only delays yield), stranger-caused sIMD hold (the recorded info item), Zap sandwiching (caller sets minimum and deadline), price-mark griefing (vault rounding makes the price monotone, so no stranger can trigger PriceFell).\n- Invariant: Rules balance covers totalEscrowed on every path; Endowments buffer equals pendingTotal and booked shares never exceed sIMD held; each depositor gets exactly its principal; yield only reaches the fixed target. Scratch probes confirmed harvest and withdrawal commute to within 64 share-wei of dust, 50 tiny in-kind exits wear the cushion by 48 share-wei, and a buffered donation followed by harvest and stake leaves depositors whole.\n- Flow Gap: desk pull measured exactly in Rules.fire, returns credited only from the desk's books, meter must pull the whole harvest, pot chip read from balance with a fixed gas allowance, Zap delta sign and settle checks, buffer-first withdrawals handing one depositor's IMD to another while shares back the newcomer.\n\n**Not reached.** The real CrowdPots, Questions and WorkOrders sources are not in this tree, so the seams that depend on them (gas of `remaining` for a series under 50,000, `fundFor` existing on the live Questions, series ids counting in `potCount`) rest on the integration suite's claims rather than my own trace.\n\n**Coverage record.** All 19 listed entry points have rows: 18 hold, `Endowments.harvest` points at the info finding, plus four invariant rows. The scratch test directory was removed since nothing in it backs a finding.","createdAt":"2026-10-15T13:02:09.764Z","verdict":null,"findings":[{"severity":"info","title":"Unbounded liveness grief: a stranger can renew sIMD's one-block hold on Endowments every block, stopping every harvest and every IMD withdrawal that needs the vault (already documented and kept by the","path":"src/endowments/Endowments.sol","line":894,"description":"Every vault exit of Endowments redeems or withdraws from address(this). sIMD stamps a holder with the block in which it was minted shares, and a transfer carries a newer stamp to the receiver, so anyone who calls sIMD.deposit(1, Endowments) (about 60,000 gas) in a block makes sIMD's maxRedeem(Endowments) zero for that block. While that is repeated every block, harvest(id) and withdraw(id, amount) for any amount above the endowment's buffer revert with the vault's own error, and no yield reaches any target. Principal is not at risk: exit(id, amount, minOut, true) hands over sIMD shares in any block and the receiver can redeem them next block, and buffered IMD is still paid as IMD. The requester documents this in docs/THREATS.md E3 and ADAPTATION.md (finding fb892c57, kept as is) and tests it in test_audit_aStrangerCanHoldTheVaultEveryBlock_andOnlyTheInKindExitIsOpen. It is recorded here only so the economics review shows it was weighed: cost to the griefer is roughly 60,000 gas per block with nothing gained, the loss is delay of yield and the inconvenience of leaving in kind, and no principal moves to a wrong party. No change is asked for.","reproduction":"State: an endowment with staked principal and harvestable yield (deposit 100e18, stake, roll one block, send the vault 0.1% of its assets as yield). Each block: stranger calls IMD.approve(sIMD, 1) then sIMD.deposit(1, address(Endowments)). In that block harvest(id) reverts with sIMD's RedeemMoreThanMax / WithdrawMoreThanMax (not HoldActive), and withdraw(id, 100e18) reverts the same way; exit(id, 100e18, 100e18, true) succeeds and transfers sIMD shares worth at least 100e18 IMD. Expected per the contract's own design: exactly this; the finding is a liveness bound the design accepts, not a loss.","snippet":"            agrees = SIMD.redeem(shares, address(this), address(this)) >= assets;","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"Endowments.createForMeter(bytes32)","verdict":"holds","why":"Pulls SEED by measured transfer, refuses zero feed and a feed Questions states no cost for; SEED is buffered and is nobody's principal, so no depositor can claim it."},{"target":"Endowments.createForPot(uint256,address)","verdict":"holds","why":"Refuses the six stranding beneficiaries and an id above potCount; the fallback beneficiary is a creator-fixed trust assumption (E6), and only yield ever reaches it."},{"target":"Endowments.createForRecipient(address)","verdict":"holds","why":"Same seed and stranding checks; recipient fixed for good, harvest sends only shares beyond principal plus MARGIN."},{"target":"Endowments.deposit(uint256,uint256)","verdict":"holds","why":"Settles first, refuses a short or spare-less endowment, books principal 1:1 against buffered IMD measured on arrival; a depositor can never deposit into another's loss and gets back exactly what it put in (probes: round trip, buffer-pays-other, donation-then-harvest-then-stake)."},{"target":"Endowments.donate(uint256,uint256)","verdict":"holds","why":"Nobody's principal; it first covers any shortfall and the rest is harvestable to the target; cannot be withdrawn by anyone."},{"target":"Endowments.exit(uint256,uint256,uint256,bool)","verdict":"holds","why":"Capped at the caller's own principal; exact plan burns previewWithdraw shares and leaves the rest covered (wear of at most one share-wei per call, 48 in 50 tiny in-kind exits); pro-rata plan rounds down for the leaver so stayers never lose; in-kind shares redeem to at least the principal; minOut is "},{"target":"Endowments.harvest(uint256)","verdict":"finding","findingRef":1,"why":"Takes only shares beyond MARGIN plus the staked principal's need; meter and pot room caps bound what leaves; LowGas floor stops starving the pot; commutes with withdraw to within dust (64 share-wei). Only the documented stranger-hold liveness grief applies (finding 1, info)."},{"target":"Endowments.stake()","verdict":"holds","why":"Anyone, every 50 blocks; books the batch at previewDeposit and reverts if the vault mints less; price mark only rises under the vault's own rounding, so PriceFell cannot be triggered by a stranger; a stake that can never run leaves buffers withdrawable as IMD."},{"target":"Endowments.withdraw(uint256,uint256)","verdict":"holds","why":"Exact-or-revert form of exit: pays buffer first then the vault, reverts HoldActive in the stake block and BelowMinimum rather than short-change anyone; principal comes back to the wei in every probe."},{"target":"Rules.cancel(uint256)","verdict":"holds","why":"Creator only; returns exactly the rule's escrow and reduces totalEscrowed by the same; later desk returns for its orders go straight to the creator via reconcile."},{"target":"Rules.fire(uint256,bytes)","verdict":"holds","why":"Spends only the rule's own escrow (maxPay or pay amount plus tip), pays the tip to the first firer by design (R12, requester asks to keep), condition read with the full gas allowance so the firer cannot pick the answer; feed rules fire once per round; desk must pull exactly maxPay."},{"target":"Rules.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Desk only; unknown order ignored; credits from the desk's returned figure under the balance ceiling, so a failed callback loses nothing (reconcile does the same later)."},{"target":"Rules.recall(uint256,uint256)","verdict":"holds","why":"Creator only and only the rule's own order; the desk decides whether cancel is still allowed; the amount credited is read from the desk's books after the cancel, under the lock."},{"target":"Rules.reconcile(uint256,uint256)","verdict":"holds","why":"Credits returned minus credited, never a balance delta; the ceiling keeps booked escrows within holdings; a cancelled rule's returns go to its creator; repeatable and idempotent."},{"target":"Rules.topUp(uint256,uint96)","verdict":"holds","why":"Anyone may fund a live rule; the gift becomes the creator's on cancel; no path lets the funder take it back or redirect it; IMD has no transfer fee (T1)."},{"target":"Zap.buy(uint256,uint256)","verdict":"holds","why":"Swaps exactly msg.value, enforces the caller's minImdOut and deadline, returns every wei of IMD and unspent ETH to the caller; keeps nothing between calls."},{"target":"Zap.chipIn(uint256,uint256,uint256)","verdict":"holds","why":"Chip and its refund belong to msg.sender via chipInFor; allowance exactly imdOut and reset after; a pot that takes nothing reverts the whole call so the ETH stays with the caller; surplus IMD returned."},{"target":"Zap.fundMeter(bytes32,uint256,uint256)","verdict":"holds","why":"Meter shares credited to msg.sender via fundFor; zero shares reverts; allowance reset; any IMD the meter left returned to the caller."},{"target":"Zap.unlockCallback(bytes)","verdict":"holds","why":"PoolManager only and only inside Zap's own unlock; checks delta signs, caps ETH spent at the ETH sent, verifies settle counted the ETH paid; no hook on the pool so nothing else runs mid-swap."},{"target":"invariant: Rules balance >= totalEscrowed and no rule spends another's escrow","verdict":"holds","why":"Every escrow change is paired with totalEscrowed; reconcile's ceiling refuses credit for IMD not present; fire deducts before any transfer."},{"target":"invariant: Endowments IMD held == pendingTotal and sum of booked shares <= sIMD held","verdict":"holds","why":"Buffer changes are paired with pendingTotal; batch split rounds down; every vault exit checks shares burnt <= planned and IMD received >= planned."},{"target":"invariant: every covered endowment pays each depositor exactly its principal; yield only ever reaches the fixed target","verdict":"holds","why":"Traced deposit/withdraw/exit/harvest in every order including buffered donations and buffer-paid withdrawals; probes show principal to the wei and harvest amounts independent of ordering."},{"target":"invariant: Zap holds nothing of a caller after a call","verdict":"holds","why":"_returnEverything sends whole balances; allowances reset; a refund the caller cannot take reverts the call."}],"summary":{"reported":true,"total":19,"answered":19,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"308a42304b2b7a431a0ae9ba228c9872855b5f207901720690a2feccfcfab1f6","nodeKey":"write_foundry_tests","role":"tests","attempt":1,"deviceKey":"672c29f92850efe0276684b5fbbd1308b6421a1f2ec2ee0c50fe13786be27e05","seat":{"tokenId":"626","agentId":"51875"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":30,"runtime":"claude","inputTokens":740,"wallClockMs":1293502,"outputTokens":36919,"cachedInputTokens":3257244},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"b27fb7750fd347415d2510c687d5984aaa17c1e4","resultTreeHash":"83e9a7d2e5d7b2dd80282e715608652a1597ecc7","bundleHash":"a8d8414d8d7d3421a9dfd7509df974995cf1c6e652834f197c88168a29ae9f21","bundleBytes":16079,"artifacts":[],"changedPaths":["test/endowments/Endowments.edges.t.sol","test/rules/RulesEdges.t.sol","test/rules/ZapDelta.t.sol"],"allowedPaths":["test","test/**"],"dependencyBundleHashes":["306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2"],"verificationErrors":0,"summary":"The suite passes in full with the verifier's seed, and the three new files are formatted and lint-clean. No defect was found, so no findings file was written.\n\n**What I added** (24 tests, all under test/, on top of the 417 already there):\n\n- `test/rules/RulesEdges.t.sol` covers the 96-bit escrow ceiling: a top-up past it reverts with a checked-arithmetic panic and changes nothing, a deposit exactly at it fires, and a desk return that cannot be booked into a full escrow is freed by cancel. It also pins the escrow-at-cost line as a fuzz, the widest cooldown, order lifetime and feed age a creator can set, two rules sharing one feed each firing once per round, a feed whose rounds go backwards, an answer exactly at its maximum age, unknown ids on every function, and a rule whose condition reads Rules' own books under the transient lock.\n- `test/rules/ZapDelta.t.sol` drives the swap callback with a scripted pool manager that returns any packed 256-bit delta. A 2000-run fuzz over both signed 128-bit halves checks Zap accepts exactly the well-signed results within the ETH sent and pays out what the halves say. The corners of each half are pinned, including the lower half's sign bit not leaking into the ETH side.\n- `test/endowments/Endowments.edges.t.sol` covers the stake interval and pot-id boundaries with exact revert arguments, the views outside their domain, one depositor's withdrawal being paid from another's fresh deposit with the other then backed by shares and still paid to the wei, the creator's seed being non-refundable and harvested to the target, and an endowment everyone has left taking deposits again.\n\n**One edge worth knowing, not a defect:** a rule whose escrow sits at the uint96 ceiling cannot book a desk return through reconcile until the creator cancels. That needs about 79 billion IMD in one escrow and is recoverable, so I tested it rather than reporting it.\n\n**Still owed:** the fork runs under the fork profile against live mainnet state, which cannot run here without network.","createdAt":"2026-10-15T12:50:41.229Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+8fbf00a5","verifiedTreeHash":"83e9a7d2e5d7b2dd80282e715608652a1597ecc7","at":"2026-10-15T12:53:36.674Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"77a4adde8e7ba79d964cb0d9b4141081376eb3484181337feda01b0fa052427c","nodeKey":"audit_permissions","role":"review","attempt":1,"deviceKey":"5dd3af20ad7ac0c2de926026089923d87ad708bad8a2190e151476566619bf41","seat":{"tokenId":"268","agentId":"53934"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":34,"runtime":"claude","inputTokens":482,"wallClockMs":1115928,"outputTokens":42031,"cachedInputTokens":2086827},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"b27fb7750fd347415d2510c687d5984aaa17c1e4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2"],"verificationErrors":0,"summary":"The review is complete and the findings file is written. No source file was changed, and the scratch test was removed.\n\n**Outcome.** The permission model of Rules, Endowments and Zap holds. I found no permission bypass, no confused deputy, no asymmetry that moves money to the wrong party, and no trust gap between the three contracts and their live siblings. Two minor issues are recorded in `.imd-findings.json`, one low and one informational.\n\n**What I checked, in my area.**\n- Every state-changing entry point (19 listed, plus `Rules.createRule`, which the verifier's list omits) was traced for its guard, its state writes and its failure paths. All 22 coverage rows are answered: 20 hold, 2 point to the findings.\n- Pashov's Access Control, Trust Gap and Asymmetry guides were applied item by item: inconsistent guards on shared storage, confused deputies through token approvals, paired functions (deposit/exit, stake/settle, create/harvest, fire/cancel/reconcile), branch diffs, and the seams between access, economics and asymmetry.\n- Trust roots on mainnet: all six manifest addresses carry code, sIMD's asset is IMD, the desk's constants match the mocks, every selector called on Questions, WorkOrders, CrowdPots, sIMD and the PoolManager is present in the live bytecode, and the fixed v4 pool exists with liquidity.\n- The full suite passes on this tree.\n\n```\n417 tests passed, 0 failed (20 suites)\n```\n\n**Findings.**\n1. **Low, `Endowments.createForPot`.** A pot endowment can be created for a pot that already needs nothing, since only `potId <= potCount` is checked. Every harvest then goes to the fallback beneficiary, who is chosen by the creator, while the Created event labels it a pot endowment. This is asymmetric with `createForMeter`, which refuses a feed that cannot absorb yield. Reproduced against the repository's mock pots. Suggested fix: revert when `remaining(potId)` is zero at creation.\n2. **Info, `Rules.createRule`.** Unused condition fields are refused, but unused action fields are accepted and published in `RuleCreated`, so a HIRE rule's event can name a PAY recipient that the stored rule ignores. No funds are affected. Reproduced with a scratch test.\n\n**Not reached.** I could not verify, without the earlier launches' sources, whether the real `CrowdPots.chipInFor` for a pot-completing chip fits the 500,000 gas Endowments allows. If it does not, a harvest that would complete a pot pays the fallback instead. This is a lead for the judge, not a finding. The two documented design choices the author asked to keep, the tip to the first firer and harvests paid to contract targets, were checked and behave as described.","createdAt":"2026-10-15T12:48:22.738Z","verdict":null,"findings":[{"severity":"low","title":"createForPot accepts a pot that already needs nothing, so a 'Pot' endowment can be a disguised recipient endowment for its fallback beneficiary (asymmetric with createForMeter's liveness check)","path":"src/endowments/Endowments.sol","line":326,"description":"Trust gap (access x asymmetry). createForMeter refuses a feed Questions states no cost for (line 310, FeedNotLive), so a meter endowment can only be created while its target can actually absorb yield. createForPot checks only that the pot id has been handed out (potId <= potCount). It does not ask CROWD_POTS.remaining(potId), so a pot that is already full, launched, expired or closed is accepted. For such an endowment _potRoom returns 0 on every harvest, the whole excess is offered to the pot, the pot takes nothing, and _forward pays everything to the fallback beneficiary (lines 942-947). The endowment is published in Created as kind=Pot with the pot id as target, but economically it is a Recipient endowment whose recipient is the fallback, who is chosen by the creator (and may be the creator). E6 in docs/THREATS.md says a full pot's later yield is the beneficiary's, but that describes a pot that fills after the endowment was announced; here the pot can be dead before anyone deposits, and the one on-chain check a depositor would rely on (the Created event's kind) says the yield funds a pot. No principal is at risk and the harvest goes to a party fixed at creation, hence low. Minimal fix that keeps the design: in createForPot, revert (e.g. PotNotCollecting) when CROWD_POTS.remaining(potId) == 0 at creation, mirroring createForMeter; a series always reports a non-zero remaining so it is unaffected.","reproduction":"State: CrowdPots has a single pot 1 that exists (potCount >= 1) and is full/closed, so remaining(1) == 0. Call Endowments.createForPot(1, attacker). Expected (by symmetry with createForMeter): revert, the pot cannot take yield. Actual: succeeds, emits Created(id, Kind.Pot, bytes32(1), attacker, creator). Then after deposits, stake and yield, any harvest(id): _harvestPlan room == 0 -> full excess; _forward: chipInFor takes 0 -> FallbackPaid(id, attacker, amount) with the whole harvest. Reproduced with the repository's MockCrowdPots: pots.open(1, 0); assertEq(pots.remaining(1), 0); e.createForPot(1, 0xBEEF) returns an id whose kind is Pot (test passed on this tree).","snippet":"        if (potId == 0 || potId > CROWD_POTS.potCount()) revert UnknownPot(potId);","citation":"resolved","blocking":false},{"severity":"info","title":"createRule refuses unused condition fields but accepts and publishes unused action fields (a PAY half on a HIRE rule, a HIRE half on a PAY rule)","path":"src/rules/Rules.sol","line":424,"description":"Branch asymmetry in createRule. For the condition, fields that do not apply are rejected with FieldNotUsed (line 380 for VIEW, line 383 for FEED), so a stored rule and its RuleCreated event agree. For the action, only the matching half is stored (lines 416-421) but nothing rejects the other half, and the event publishes the caller's whole RuleParams. A HIRE rule can therefore be created with p.pay = {recipient: X, amount: Y}: getRule(id).pay is all zeros, while RuleCreated.params.pay names X and Y. The NatSpec of RuleParams says 'Only the matching one of hire and pay is read and stored' but does not say the other is published. No funds move on the unused half (fire reads r.pay / r.hire from storage, which are zero), so this is informational: it only lets a creator publish misleading terms to indexers, keeper UIs and pages that decode RuleCreated instead of calling getRule. Minimal fix: in the ACT_HIRE branch revert FieldNotUsed when p.pay.recipient != 0 || p.pay.amount != 0; in the ACT_PAY branch revert FieldNotUsed when any field of p.hire is set (or at least kindHash/maxPay/orderTtl), mirroring the condition checks.","reproduction":"createRule with when = VIEW/GE on any target, action = ACT_HIRE, hire = {kindHash: keccak256('audit'), minPay 1e18, maxPay 2e18, deliverySecs 3 days, bond 0.25e18, orderTtl 1 day, flags 0}, pay = {recipient: 0xBEEF, amount: 5e18}, spec 'do the thing', deposit 0. Expected (by symmetry with lines 380/383): revert FieldNotUsed. Actual: succeeds; getRule(id).pay.recipient == address(0) while the decoded RuleCreated.params.pay.recipient == 0xBEEF and amount == 5e18. Verified with a scratch test on this tree against MockDesk (passed).","snippet":"        emit RuleCreated(id, msg.sender, p, spec, deposit);","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"Endowments.createForMeter(bytes32)","verdict":"holds","why":"open to anyone; pulls SEED from caller with balance check; refuses zero feed and a feed Questions prices at 0; stores target once, no setter"},{"target":"Endowments.createForPot(uint256,address)","verdict":"finding","findingRef":1,"why":"open; beneficiary checked against the six stranding addresses but the pot is only checked to exist, not to be collecting"},{"target":"Endowments.createForRecipient(address)","verdict":"holds","why":"open; recipient fixed for good, _strands refuses the six known sinks; creator keeps no power afterwards"},{"target":"Endowments.deposit(uint256,uint256)","verdict":"holds","why":"open; settles first, refuses a short/unspared endowment, credits principal before _pull which checks the full amount arrived"},{"target":"Endowments.donate(uint256,uint256)","verdict":"holds","why":"open; gift is nobody's principal, only raises buffer; same _pull check"},{"target":"Endowments.exit(uint256,uint256,uint256,bool)","verdict":"holds","why":"limited to caller's own principalOf; exact path keeps coverage for the rest, pro-rata path only when short; inKind never blocked by hold; minOut respected"},{"target":"Endowments.harvest(uint256)","verdict":"holds","why":"open; takes only shares above principal+MARGIN, pays only the fixed target/fallback never the caller; LowGas floor stops gas-starving the pot; approvals reset"},{"target":"Endowments.stake()","verdict":"holds","why":"open, 50-block interval; books previewDeposit and reverts if the vault mints less; WrongVault/PriceFell guards; approval consumed exactly"},{"target":"Endowments.withdraw(uint256,uint256)","verdict":"holds","why":"same path as exit with minOut=amount and inKind=false; cannot touch another depositor's principal or another endowment"},{"target":"Rules.cancel(uint256)","verdict":"holds","why":"creator only; zeroes escrow and totalEscrowed before transfer; later returns go to creator via _reconcile"},{"target":"Rules.fire(uint256,bytes)","verdict":"holds","why":"open by design; pays only fixed recipient/desk plus tip to caller; GasShort stops condition gas games; spec hash pinned; desk pull amount and order id checked"},{"target":"Rules.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"desk only (immutable WORK_ORDERS); unknown order ignored; credits from desk's returned figure under balance ceiling"},{"target":"Rules.recall(uint256,uint256)","verdict":"holds","why":"creator only and order must be the rule's; desk decides cancellability; reconcile after the desk call under the lock"},{"target":"Rules.reconcile(uint256,uint256)","verdict":"holds","why":"open; order link and desk employer both checked; credits delta of returned only; never pays the caller"},{"target":"Rules.topUp(uint256,uint96)","verdict":"holds","why":"open gift to a live rule, becomes creator's on cancel; refused for cancelled rules so nothing is stranded"},{"target":"Zap.buy(uint256,uint256)","verdict":"holds","why":"open, payable; swaps exactly msg.value, caller's minOut/deadline; whole IMD and ETH balance returned to caller"},{"target":"Zap.chipIn(uint256,uint256,uint256)","verdict":"holds","why":"open, payable; chip made in msg.sender's name via chipInFor; reverts if nothing chipped; approval reset; surplus returned to caller"},{"target":"Zap.fundMeter(bytes32,uint256,uint256)","verdict":"holds","why":"open, payable; shares credited to msg.sender via fundFor; reverts on zero shares; approval reset; surplus returned"},{"target":"Zap.unlockCallback(bytes)","verdict":"holds","why":"PoolManager only, and v4 calls back only the unlocker; delta signs and ethSpent<=ethIn checked; runs inside the locked entry point"},{"target":"Rules.createRule((...),bytes,uint96)","verdict":"finding","findingRef":2,"why":"not in the verifier's list; open; validation of condition fields complete, action half asymmetric (info)"},{"target":"invariant: constructor arguments and trust roots","verdict":"holds","why":"all six manifest addresses have code on mainnet; sIMD.asset()==IMD; desk IMD/MIN_BOND/MIN_DELIVERY/MAX_DELIVERY/flags match mocks; every selector called on Questions, WorkOrders, CrowdPots, sIMD and PoolManager is in the live bytecode; fixed v4 pool (fee 10000, spacing 200) exists with liquidity"},{"target":"invariant: no owner / no standing approvals / no stored msg.sender","verdict":"holds","why":"no constructor stores msg.sender; every forceApprove is consumed or reset in the same call; no delegatecall/selfdestruct"}],"summary":{"reported":true,"total":19,"answered":19,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"2b6ef932b31c4af02be7a06e9826e75fe81dd7ab98a4343e30baa3cb341414be","nodeKey":"audit_flow","role":"review","attempt":1,"deviceKey":"c872aaeb2e58b0807d47a9f1fc4ce6328994ba6566c35ae5c14ddb02f5655265","seat":{"tokenId":"19","agentId":"51135"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-opus-5-5","turns":21,"runtime":"claude","inputTokens":40,"wallClockMs":837848,"outputTokens":27091,"cachedInputTokens":1610084},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"b27fb7750fd347415d2510c687d5984aaa17c1e4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2"],"verificationErrors":0,"summary":"I found one low-severity issue and nothing critical, high or medium. The results are in `.imd-findings.json`, with a coverage row for each of the 19 entry points. I didn't run any tests: an attempt at `forge build` was stopped after 500 seconds, so no build finished. The finding therefore has no test file; at low severity it doesn't need one.\n\n**The finding: an endowment can pay its yield to Zap, and the next caller of Zap takes it** (`src/endowments/Endowments.sol:626`)\n- **Cause:** Endowments refuses recipients it knows would lose IMD, but it can't refuse Zap because no contract in this launch refers to another. At the end of every `buy`, `chipIn` or `fundMeter`, Zap sends its whole IMD balance to the caller (`src/rules/Zap.sol:270-271`).\n- **Reproduction:** Alice calls `createForRecipient(zap)` and deposits. Once yield builds up, Mallory calls `Endowments.harvest(id)` and then `zap.buy{value: 0.001 ether}(0, type(uint256).max)` in the same transaction, and receives the whole harvest. `createForPot(potId, zap)` behaves the same way when the pot takes nothing, and so does a Rules PAY rule that names Zap as its recipient.\n- **Impact:** Depositors' principal is safe; only the yield (or the PAY amount) goes to the wrong party.\n- **Fix that keeps the design:** have Zap return only what the current call produced, rather than its whole balance. Otherwise, warn about Zap in the docs and pages.\n\n**What holds:**\n- **Rules:** the escrow accounting, the gas floor on condition reads, the exact check of what the hire desk pulls, the job-text character check, and the books built from the desk's own figures in `reconcile` and `recall`.\n- **Endowments:** `exit` and `withdraw` in both payout paths, the share split when IMD is staked, the margin `harvest` must leave, the price-drop guard, and the gas floors for paying into crowd pots.\n- **Zap:** the swap callback (signs of the amounts, the ETH it pays never above what was sent, settle and take) and the refunds.\n\n**Checked live on mainnet with `cast`:**\n- IMD's symbol is \"IMD\", and sIMD's underlying token is IMD.\n- Questions, WorkOrders and CrowdPots all have code, including the functions Zap and Rules call (`fundFor`, `chipInFor`, `MAX_DELIVERY`).\n- The ETH/IMD pool that Zap's code names (fee 1%, tick spacing 200, no hook) exists.\n\n**Not covered:**\n- I couldn't get the source of the live WorkOrders and Questions contracts (Sourcify has no match), so the desk's own checks are known only through the repository's stand-in contracts.\n- I didn't verify sIMD's code. Endowments' price-drop guard assumes sIMD's price never falls in normal use.","createdAt":"2026-10-15T12:37:02.009Z","verdict":null,"findings":[{"severity":"low","title":"Endowments accepts this launch's own Zap as recipient or fallback, and Zap hands every harvest to whoever calls it next","path":"src/endowments/Endowments.sol","line":626,"description":"`_strands` refuses the addresses Endowments knows would lose or misdirect IMD sent by plain transfer, but it cannot know this launch's sibling Zap, because no contract of the launch refers to another. Zap's `_returnEverything` (src/rules/Zap.sol:270-271: `uint256 imdLeft = IMD.balanceOf(address(this)); if (imdLeft != 0) IMD.safeTransfer(msg.sender, imdLeft);`) sends its whole IMD balance to whoever calls `buy`, `chipIn` or `fundMeter` next. So with Zap as a Recipient endowment's `recipient`, or as a Pot endowment's `fallbackBeneficiary`, the yield is not lost (which the NatSpec accepts) but goes to a third party chosen by timing. `harvest` is open to anyone, so the party who triggers the payout can also collect it in the same transaction. The same applies to a Rules PAY rule whose `pay.recipient` is Zap: Rules refuses only zero and itself (Rules.sol:397). This is Pattern 1/3 of docs/THREATS.md section 10 (a payout an observer can force and redirect) through a sibling contract. Depositors' principal is not at risk; the endowment's yield, or the PAY amount, is.","reproduction":"Deploy Rules, Endowments and Zap with the manifest's arguments. 1) Alice calls Endowments.createForRecipient(address(zap)), which succeeds because _strands(zap) is false, then deposit(id, 1_000_000e18); stake() runs and sIMD accrues yield so that harvestable(id) = Y >= MIN_HARVEST. 2) In one transaction Mallory calls Endowments.harvest(id), which safeTransfers Y IMD to Zap, and then zap.buy{value: 0.001 ether}(0, type(uint256).max). _returnEverything reads IMD.balanceOf(zap) = imdOut + Y and transfers all of it to Mallory. Expected: createForRecipient reverts BadTarget for an address that passes plain-transferred IMD on to arbitrary callers, or the yield reaches the intended target. Actual: Mallory receives Y, the endowment's whole harvest. The same steps with createForPot(potId, address(zap)) when the pot takes nothing (remaining == 0) send the whole harvest to Mallory through FallbackPaid. Possible fixes that keep the design: have Zap pay out only what the current call produced (imdOut - chipped) instead of its whole balance, so IMD that arrives by plain transfer stays put; or warn about it in createForRecipient/createForPot and the pages, with Zap's address listed beside the other refused targets in docs.","snippet":"        return to == address(0) || to == address(this) || to == address(IMD) || to == address(SIMD)","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"Endowments.createForMeter(bytes32)","verdict":"holds","why":"zero feed and costOf==0 refused; SEED buffered and pulled with balance check; nonReentrant"},{"target":"Endowments.createForPot(uint256,address)","verdict":"finding","findingRef":1,"why":"pot id bounded by potCount (shared pot/series id space); fallback may be the sibling Zap, which passes the IMD to its next caller"},{"target":"Endowments.createForRecipient(address)","verdict":"finding","findingRef":1,"why":"refuses the 6 known strand addresses but accepts the sibling Zap, whose sweep hands harvests to any caller"},{"target":"Endowments.deposit(uint256,uint256)","verdict":"holds","why":"settles first, requires _whole, principal and buffer move together, _pull checks arrival"},{"target":"Endowments.donate(uint256,uint256)","verdict":"holds","why":"settles first; buffer only, not principal; later harvest takes it via _keep(principal,buffered)"},{"target":"Endowments.exit(uint256,uint256,uint256,bool)","verdict":"holds","why":"traced exact/pro-rata _plan branches, hold check, minOut, state before transfers, _leaveVault balance checks"},{"target":"Endowments.harvest(uint256)","verdict":"finding","findingRef":1,"why":"keep=MARGIN+previewWithdraw(staked principal) protects principal; meter/pot caps and gas floors traced; recipient may be Zap (finding 1)"},{"target":"Endowments.stake()","verdict":"holds","why":"interval, asset check, price mark, epoch booked at previewDeposit and minted/balance verified; split rounds down in _settle"},{"target":"Endowments.withdraw(uint256,uint256)","verdict":"holds","why":"_exit with minOut=amount; never pays less than amount, reverts instead"},{"target":"Rules.cancel(uint256)","verdict":"holds","why":"creator only, once; escrow zeroed and totalEscrowed reduced before transfer"},{"target":"Rules.fire(uint256,bytes)","verdict":"holds","why":"status/cooldown/condition with gas floor, escrow debited before action, spec hash, desk pull checked exactly, order linked once; tip to first firer is the stated design"},{"target":"Rules.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"desk-only, unknown orders ignored, reconciles from desk books under the lock"},{"target":"Rules.recall(uint256,uint256)","verdict":"holds","why":"creator and order link checked; desk cancel then _reconcile reads returned figure"},{"target":"Rules.reconcile(uint256,uint256)","verdict":"holds","why":"credits returned-credited once, employer must be Rules, balance ceiling keeps escrows <= balance; cancelled rule pays creator"},{"target":"Rules.topUp(uint256,uint96)","verdict":"holds","why":"existing, uncancelled rule; checked uint96 add; pulled after books updated under lock"},{"target":"Zap.buy(uint256,uint256)","verdict":"finding","findingRef":1,"why":"swap and refunds correct; sweeping the whole IMD balance to the caller is what lets plain-transferred harvests/PAYs be taken (finding 1)"},{"target":"Zap.chipIn(uint256,uint256,uint256)","verdict":"holds","why":"chip made in caller's name, allowance reset, surplus IMD and unused ETH returned"},{"target":"Zap.fundMeter(bytes32,uint256,uint256)","verdict":"holds","why":"fundFor credits caller (selector 0x0bbc7e6e present in live Questions bytecode); allowance reset; leftovers returned"},{"target":"Zap.unlockCallback(bytes)","verdict":"holds","why":"PoolManager-only; delta signs checked, ethSpent<=ethIn, settle/take balance the deltas; pool key fee 10000/tick 200 exists on mainnet (slot0 lpFee 0x2710)"},{"target":"invariant: constructor arguments match live mainnet contracts","verdict":"holds","why":"cast: IMD symbol 'IMD'; sIMD asset()==IMD; Questions/WorkOrders/CrowdPots have code and the selectors Zap/Endowments call"}],"summary":{"reported":true,"total":19,"answered":19,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"593e5020258cf0a6dbdd089236cd3fb3d6f667cb2ae9a8cadb4d29c1bd5645c5","nodeKey":"audit_math","role":"review","attempt":1,"deviceKey":"a11137177fc72d8bc1a019c65c97a6f883784a7e9fc180fc790419b6cffc1eec","seat":{"tokenId":"967","agentId":"52396"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":33,"runtime":"claude","inputTokens":450,"wallClockMs":803026,"outputTokens":41121,"cachedInputTokens":1983019},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"b27fb7750fd347415d2510c687d5984aaa17c1e4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":["306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2"],"verificationErrors":0,"summary":"The review is complete. The findings file is written, the scratch tests are removed, and the working tree is otherwise untouched.\n\n**Outcome.** No substantiated money-losing defect in the Math Precision, Boundary or Numerical Gap areas. I recorded one informational note and a full coverage record (all 19 entry points plus two invariants) in `.imd-findings.json`.\n\n**What I checked, with concrete values**\n\n- **Endowments share math.** At the fixture's vault state one IMD-wei is about 125,572 share-wei, so `MARGIN` (1e12 share-wei) and `SEED` (1e12 IMD-wei, about 1.26e17 share-wei) are far larger than the rounding they absorb. A batch split costs an endowment under 3 share-wei, a withdrawal at most 1. I verified with scratch tests that a 1-wei deposit round-trips exactly, that 600 tiny exits after a harvest wear at most one share-wei each, and that a harvest drained to exactly `MARGIN` by a donation leaves the endowment whole after the next stake.\n- **Rules escrow math.** Every fire cost is bounded by `CostTooHigh` at creation, the uint96 downcast follows a `held >= cost` check, and uint96 additions revert rather than wrap at 2^96 IMD-wei, which is unreachable. The 32-byte-at-a-time spec filter is carry-free for every allowed byte and walks refused words byte by byte, so its positions match the desk's.\n- **Zap delta unpacking.** The arithmetic shift and int128 truncation reproduce v4's packed-delta signs, `ethSpent <= ethIn` is enforced, and the price limit is one above the minimum sqrt price.\n- **Boundary lead closed.** `createRule` does not pre-check `orderTtl` against the desk, but the desk only requires a future expiry, which the nonzero-TTL check guarantees.\n\n**The one note (info).** The pot branch of `_harvestPlan` has no `MIN_HARVEST` floor on the pot's `remaining`, unlike the meter branch. With the pot one wei short of full, `harvestable` returns 1 and `harvest` succeeds, burning about 124k share-wei and roughly 900k gas to move one wei. No funds are lost, so it is a consistency and keeper-cost observation only.\n\n**Not reached.** The real CrowdPots and Questions sources are not in this tree, so the 50k gas allowance for `remaining` and any minimum-chip or minimum-fund rule on the live contracts could only be checked through the project's own integration tests, not independently.","createdAt":"2026-10-15T12:36:18.736Z","verdict":null,"findings":[{"severity":"info","title":"Pot harvest has no MIN_HARVEST floor on the pot's room: a pot that needs 1 wei makes harvest spend ~900k gas to move 1 wei","path":"src/endowments/Endowments.sol","line":756,"description":"The meter branch of `_harvestPlan` refuses a harvest whose cut amount is below MIN_HARVEST (`room < MIN_HARVEST ? 0 : ...`, line 739), so a nearly full meter does not trigger a whole vault withdrawal for dust. The pot branch has no such cut: any nonzero `room` below `worth` becomes the harvest amount. `harvestable` reports the same value, so a keeper that acts on `harvestable(id) > 0` pays for a full vault withdrawal, the gas-floored 500k pot call and a Harvested/Settled event set to deliver one wei. Nothing is lost (the shares burned are exactly previewWithdraw(1) = ~124k share-wei, worth one wei, out of the yield; the fallback gets nothing) and the pot is then full so the next harvest pays the fallback, so this is a consistency and keeper-cost note, not a defect in custody. Boundary: `room` in (0, MIN_HARVEST) is treated differently by the two target kinds, and the documentation of `MIN_HARVEST` (\"The least IMD a harvest is run for\") does not hold on the pot path.","reproduction":"Offline suite state (EndowmentsBase): pot POT needs 1000e18. alice deposits 100e18 into the pot endowment; stake; wait a block; add 1% yield to the vault. A stranger chips 1000e18 - 1 directly into the pot, so `remaining(POT) == 1`. Now `endow.harvestable(potId)` returns 1 and `endow.harvest{gas: 850_000}(potId)` succeeds, returns 1, the pot receives 1 wei, the fallback beneficiary receives 0, and the endowment's shares drop by 124330 (previewWithdraw(1)). Expected by the MIN_HARVEST doc comment: NothingToHarvest, as the meter branch gives for room < 1e15. Actual: a one-wei harvest costing the caller ~900k gas.","snippet":"            if (room != 0 && room < worth) {\n                uint256 part = SIMD.previewWithdraw(room);","citation":"resolved","blocking":false}],"coverage":{"rows":[{"target":"Endowments.createForMeter(bytes32)","verdict":"holds","why":"No arithmetic beyond ++endowmentCount and SEED buffering; SEED pulled in full (_pull balance check); costOf==0 refused."},{"target":"Endowments.createForPot(uint256,address)","verdict":"holds","why":"potId bounds 1..potCount checked; bytes32(potId) round-trips; SEED buffered and pulled exactly."},{"target":"Endowments.createForRecipient(address)","verdict":"holds","why":"Only sentinel refusals and SEED pull; no division or cast."},{"target":"Endowments.deposit(uint256,uint256)","verdict":"holds","why":"_whole gate (previewWithdraw(P-B)+SPARE) is invariant under adding amount to both P and B; principal and buffer sums exact; _pull insists on full arrival. 1-wei deposit round-trips to the wei (scratch test)."},{"target":"Endowments.donate(uint256,uint256)","verdict":"holds","why":"Adds to buffer only; a donation >= principal lets harvest take all but MARGIN and the endowment is whole again after the next stake (scratch test)."},{"target":"Endowments.exit(uint256,uint256,uint256,bool)","verdict":"holds","why":"Covered path: previewWithdraw(a)+previewWithdraw(b) <= previewWithdraw(a+b)+1 so at most one share-wei of margin per exit (checked: 600 one-to-three-wei exits wear <= 1 share-wei each); short path mulDiv pro-rata rounds down for the leaver; vaultOut <= staked so no underflow; previewRedeem==0 keeps "},{"target":"Endowments.harvest(uint256)","verdict":"finding","findingRef":1,"why":"Share math holds (keep = MARGIN + previewWithdraw(P-B); byAssets part = ceil(room*r) < excess whenever r > 1); only the pot branch lacks the MIN_HARVEST cut on room (finding 1, info)."},{"target":"Endowments.stake()","verdict":"holds","why":"Batch booked at previewDeposit (floor); minted >= shares and balance checks; per-endowment split mulDiv floor so sum <= batch; r.assets nonzero so _settle never divides by zero; price mark monotone under vault rounding."},{"target":"Endowments.withdraw(uint256,uint256)","verdict":"holds","why":"Same plan as exit with minOut = amount; while isWhole any part of one's principal is paid exactly (margin bound above); HoldActive only when vault shares are needed."},{"target":"Rules.cancel(uint256)","verdict":"holds","why":"refund = escrow, totalEscrowed -= refund exact; once only."},{"target":"Rules.fire(uint256,bytes)","verdict":"holds","why":"cost = spend + tip in uint256 and bounded by CostTooHigh at creation; escrow downcast after held >= cost; fires+1 cannot overflow since fires < maxFires <= 2^32-1; cooldown and freshness boundaries inclusive as documented; SWAR spec filter verified byte-wise (carry-free for allowed bytes, refused wo"},{"target":"Rules.onWorkDelivered(uint256,bytes16,bytes32)","verdict":"holds","why":"Desk-only; credits returned - credited from the desk's books under the balance ceiling; uint96 add overflow only at 2^96 IMD-wei, unreachable."},{"target":"Rules.recall(uint256,uint256)","verdict":"holds","why":"Creator-only; books written from accountOf after the desk cancels; same _reconcile arithmetic."},{"target":"Rules.reconcile(uint256,uint256)","verdict":"holds","why":"fresh = returned - credited only when returned > credited; balance ceiling totalEscrowed + amount keeps escrows within holdings; cancelled rule paid directly without touching totalEscrowed."},{"target":"Rules.topUp(uint256,uint96)","verdict":"holds","why":"uint96 escrow += amount reverts rather than wraps; totalEscrowed in uint256; cancelled rule refused."},{"target":"Zap.buy(uint256,uint256)","verdict":"holds","why":"deadline inclusive, msg.value == 0 refused, imdOut == 0 or < minImdOut refused; whole IMD and ETH balances returned; refund failure only reverts when ETH is actually owed."},{"target":"Zap.chipIn(uint256,uint256,uint256)","verdict":"holds","why":"Approval capped at imdOut so the pot cannot take more; chipped == 0 refused; surplus IMD and unspent ETH returned by whole-balance sweep."},{"target":"Zap.fundMeter(bytes32,uint256,uint256)","verdict":"holds","why":"Same swap and sweep; shares == 0 refused; allowance reset to zero."},{"target":"Zap.unlockCallback(bytes)","verdict":"holds","why":"delta >> 128 is an arithmetic shift and int128 truncation keeps each packed half's sign; eth > 0 or imd < 0 refused; ethSpent <= ethIn enforced; settle return compared to ethSpent; PRICE_LIMIT = MIN_SQRT_PRICE+1 valid for zeroForOne."},{"target":"invariant: sum of endowment shares <= SIMD.balanceOf(Endowments)","verdict":"holds","why":"Batch split floors, _leaveVault bounds burned shares by the plan, transfers out are exact."},{"target":"invariant: Rules.totalEscrowed <= IMD.balanceOf(Rules)","verdict":"holds","why":"Every credit passes the balance ceiling; every debit is paid out or pulled exactly (DeskPulledWrongAmount)."}],"summary":{"reported":true,"total":19,"answered":19,"ratio":1,"missing":[],"unreached":[]}}},{"hash":"8db3dc487a713de6645b2da8f57495d88966f1d8c886413280b6fe7510a9f034","nodeKey":"manifest","role":"integrate","attempt":2,"deviceKey":"68fd8dd6450ac70c17815a4b8406630872b015196d49e05b17fe03c8fb319fe6","seat":{"tokenId":"858","agentId":"51502"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"gpt-6-astra","turns":6,"runtime":"codex","inputTokens":76620,"wallClockMs":310162,"outputTokens":4004,"cachedInputTokens":731520},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"b27fb7750fd347415d2510c687d5984aaa17c1e4","resultTreeHash":"25c2b49389ca704b35772aec4e3fc25b8434dbdf","bundleHash":"49b7dcc4e6e4b727b5abed0bb98b5a20e06f936096d89aee738c543d3c5ccbaf","bundleBytes":8564,"artifacts":[],"changedPaths":["launch.json"],"allowedPaths":["launch.json"],"dependencyBundleHashes":["306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2"],"verificationErrors":0,"summary":"Created `launch.json` exactly as requested for Rules, Endowments, and Zap.\n\nValidated all ten addresses against the source and constructor ABIs. `forge build` passed; `forge test` passed all 417 tests with the specified fuzz seed.\n\nOnly `launch.json` changed.","createdAt":"2026-10-15T12:32:15.343Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+8fbf00a5","verifiedTreeHash":"25c2b49389ca704b35772aec4e3fc25b8434dbdf","at":"2026-10-15T12:34:57.748Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"3bc31580c886dfed33faf326209001f90a0f112bf04732eac10bdc156d0380e5","nodeKey":"manifest","role":"integrate","attempt":1,"deviceKey":"629adec6a25d924166de43c9c95c405fe5a690e8fb25a66341b69a9245f8ba15","seat":{"tokenId":"682","agentId":"52568"},"outcome":"failed","accepted":false,"failureReason":"clone_failed","failureClass":"machine","safetyRefusal":false,"usage":{"model":null,"turns":0,"runtime":"codex","inputTokens":0,"wallClockMs":516,"outputTokens":0,"cachedInputTokens":0},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":["launch.json"],"dependencyBundleHashes":["306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2"],"verificationErrors":0,"summary":"clone failed: fatal: destination path '/var/lib/imd/seat-682/work/7f2023fc-fa1f-4a7f-a507-3bbc712c51d6/8911bc68-0801-4988-b43c-c858a8cf8a79' already exists and is not an empty directory.\n","createdAt":"2026-10-15T12:22:53.418Z","verdict":null,"findings":[],"coverage":null},{"hash":"af671a367377b38c1bed15111de812938beff85017b3ee59c0405c42503767a0","nodeKey":"adapt_contract_project","role":"implement","attempt":1,"deviceKey":"f494611affb5524c465de9acfe93c8b58f1526db7e318445c53c4ccad42c79a8","seat":{"tokenId":"757","agentId":"51877"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":30,"runtime":"claude","inputTokens":930,"wallClockMs":769995,"outputTokens":23376,"cachedInputTokens":2357873},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","resultTreeHash":"7639e174ff0ce22efd9107bf1536faa3ba371b59","bundleHash":"306bef66d0b05473dca29fda0d66bf741e598b99da21f7e6fa0b25f3e73d1ab2","bundleBytes":7756,"artifacts":[],"changedPaths":["ADAPTATION.md","README.md","docs/THREATS.md","test/endowments/Endowments.audit.t.sol"],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The work is complete and verified. No source file changed.\n\n**Outcome.** The project already meets every rule of the contracts-only launch: nonpayable constructors with literal address arguments, no external call or code check at construction, no owner, no token, no proxy, and no forbidden opcode in any runtime. The project's own launch suite proves each of these. Both audit findings reproduce, and neither required a code change.\n\n**What I did.**\n- Added `test/endowments/Endowments.audit.t.sol` with two tests that follow the auditor's steps exactly. The first shows a stranger holding the vault for five consecutive blocks with one wei each, with harvest and withdrawal reverting on the vault's error and the in-kind exit working throughout. The second shows a meter endowment's yield stuck after Questions stops pricing the feed, surviving the last depositor's exact withdrawal, and leaving only once the feed is priced again.\n- Tightened `docs/THREATS.md` at E3, E5 and E12 to say plainly what the auditor showed was left unsaid: the external hold has no upper bound, and a meter endowment's yield has no exit at all while its feed is not sold.\n- Updated the test count in the README and wrote `ADAPTATION.md` with each change, the reason, and why both findings were kept as they are.\n\n**Why no code change.** The hold finding states itself that no code change is required, since the hold is the vault's rule. Its optional suggestion would only rename an error path that the docs and existing tests already describe. The stranded-yield finding calls itself a documented trade-off, and its sketched fix would change a public function's signature, the launch runbook that the tests hold against the code, and the design's rule that yield has no address but the one fixed at creation. The requester asked for the code as it is, and the launch asks for the smallest diff.\n\n**Verification.** The build is clean. The full default suite passes with the verifier's fuzz seed.\n\n| Check | Result |\n|---|---|\n| forge build | clean |\n| forge test (seeded) | 417 passed, 0 failed, 20 suites |\n| forge fmt --check | clean |\n\nFork suites were not run, since they need a mainnet RPC. Nothing was committed, and no launch.json was written.","createdAt":"2026-10-15T12:19:55.738Z","verdict":{"status":"accepted","profile":"foundry","evaluation":"checks","rejectionCode":null,"detail":"all checks passed","verifierVersion":"0.1.0+8fbf00a5","verifiedTreeHash":"7639e174ff0ce22efd9107bf1536faa3ba371b59","at":"2026-10-15T12:22:43.517Z","failedChecks":[]},"findings":[],"coverage":null},{"hash":"836aa89785bb21dcd3d8ef692a8f7b9c4ffb80a83b1f720121a42e4e93d89eb2","nodeKey":"audit_imported_code","role":"review","attempt":1,"deviceKey":"4199bf0c80a9d6cf8ebddcc4323ec454ba4ea03ae2398d81fd3fec4d47f8be2a","seat":{"tokenId":"1499","agentId":"51229"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":34,"runtime":"claude","inputTokens":386,"wallClockMs":880913,"outputTokens":49348,"cachedInputTokens":2149092},"baseCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","startCommit":"01d9b26f1831195da60529df7d9a6c1e62884765","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The review is complete and `.imd-findings.json` holds the result: no critical, high or medium defects, two substantiated low findings, and one coverage note. No source file was changed and the tree is clean.\n\n**What I read.** All three contracts in full (`Rules`, `Endowments`, `Zap`), the frozen interfaces, the mainnet address library, both docs, and the stand-ins for the desk, vault, pots and token. Every state-changing entry point was walked for access, value flow, reentrancy, rounding, time and external trust. Build and the default plus fork suites pass: 442 tests, 0 failures. The manifest in docs/LAUNCH.md matches the constructors, the brief and the schema. No owner, proxy, initializer, minting, pausing, DELEGATECALL, CALLCODE or SELFDESTRUCT is present, and constructors make no external call.\n\n**What I could not reach.** The real sources of Questions, WorkOrders and CrowdPots are not in this checkout, so the integration project cannot build here. Conclusions about those three rest on the interface comments, the stand-ins, and the fork tests that call the live contracts. The invariant suites were not re-run in the time budget.\n\n**Findings, both reproduced in a scratch Foundry test:**\n\n- **Low, Endowments line 497.** Anyone can deposit 1 wei into sIMD in Endowments' name every block. That holds the vault each block, so every harvest and every IMD withdrawal reverts with the vault's error for as long as the attacker pays gas. Principal is safe via in-kind exit. Documented as accepted in THREATS.md E3, but worth a conscious decision by the panel.\n- **Low, Endowments line 777.** If Questions stops pricing a feed for good, a meter endowment's accrued yield has no exit at all: harvest reverts forever, nothing else can move the shares, and after the last depositor leaves the yield sits locked. A creator-chosen fallback, as pot endowments have, would preserve the design.\n\nNeither carries a proof file, since neither is high severity. The two rules the requester asked to keep, the tip to the first firer and harvest paying a contract target, were checked and read as correct for the stated design.","createdAt":"2026-10-15T12:00:13.006Z","verdict":null,"findings":[{"severity":"low","title":"Anyone can hold Endowments' vault every block for 1 wei, stopping harvest and IMD withdrawals indefinitely","path":"src/endowments/Endowments.sol","line":497,"description":"sIMD pays nothing out to a holder in the block it was minted shares (or received freshly minted shares). Endowments only guards against the hold its own stake() causes (lastStakeBlock). A stranger can call SIMD.deposit(1, address(endowments)) in every block: the contract is then stamped in every block, so every harvest() of every endowment reverts inside _leaveVault with the vault's RedeemMoreThanMax/WithdrawMoreThanMax, and every withdraw()/exit(inKind=false) that needs the vault reverts the same way. Cost to the attacker is 1 IMD-wei plus one deposit's gas per block (about 60k gas, a few cents at today's base fee); it can be kept up for days. No principal is lost (exit with inKind still works and hands over sIMD shares that redeem next block), but no endowment's yield can reach its meter, pot or recipient while it lasts, and ordinary depositors following the runbook's `withdraw` see an opaque vault error instead of HoldActive. docs/THREATS.md E3 accepts this; it is recorded here so the adapter and the final panel weigh the liveness trade-off knowingly. The hold is the vault's rule, so no change inside this contract can remove it; the design-preserving mitigation is to keep the inKind escape hatch prominent in the runbook (it is) and, optionally, to surface the external hold as HoldActive by checking SIMD.maxRedeem(address(this)) before calling the vault, so callers get the documented error. No code change is required.","reproduction":"State: Endowments with one recipient endowment, alice deposits 100 IMD, stake(), one block later 1% yield is transferred to the vault so harvestable(id) > 0. Each block: stranger calls simd.deposit(1, address(endow)). Then endow.vaultHold() == true; endow.harvest(id) reverts RedeemMoreThanMax (expected: yield reaches the recipient); alice's endow.withdraw(id, 1e18) reverts WithdrawMoreThanMax (expected: 1 IMD back). Repeating the 1-wei deposit each block keeps both reverting for as long as the stranger pays gas. Reproduced in a scratch Foundry test against the project's own MockStakedIMD (which follows the real vault's hold stamp) for five consecutive blocks.","snippet":"        if (block.number <= lastStakeBlock) revert HoldActive();","citation":"resolved","blocking":false},{"severity":"low","title":"A meter endowment's accrued yield is stranded for good if Questions stops pricing the feed","path":"src/endowments/Endowments.sol","line":777,"description":"_meterRoom returns zero whenever Questions.costOf(feedId) is zero (the Intake no longer sells the feed's action, or its price is above the feed's cap). _harvestPlan then reports nothing to harvest, so harvest() reverts NothingToHarvest. Nothing else can move the shares an endowment holds beyond its principal: harvest is the only exit for yield, the target is fixed, there is no sweep, and depositors receive exactly their principal. If the feed never becomes askable again the yield that accrued before and after that moment is locked in the contract forever, even after the last depositor has left (principal 0, shares > 0). The creation-time check (FeedNotLive) only guards the moment of creation. docs/THREATS.md E5/E12 says the yield 'waits'; what it does not say is that there is no path at all if the wait never ends. The design intentionally has no owner and no retargeting, so this is reported as a documented trade-off with a concrete state, not a bug in the arithmetic: the minimal change that preserves the design would be a creator-chosen fallback for a meter endowment too (as pot endowments have), used only while costOf is zero.","reproduction":"State: meter endowment for FEED (costOf 0.5 IMD at creation), alice deposits 100 IMD, stake(), next block 1% yield into the vault; harvestable(id) > 0. Then Questions reports costOf(FEED) == 0 (the project's MockQuestions.setCost(0) models the Intake no longer selling the action). harvestable(id) == 0; harvest(id) reverts NothingToHarvest. alice withdraws 100 IMD to the wei (works). endowment(id) then shows principal 0 and shares worth about 1 IMD; every later harvest(id) still reverts NothingToHarvest and no other function can move those shares. Expected: a way for the creator-chosen beneficiary to receive the yield, or at least for it to be recoverable; actual: locked for good unless the feed is sold again. Reproduced in a scratch Foundry test.","snippet":"        if (cost == 0) return 0;","citation":"resolved","blocking":false},{"severity":"info","title":"Coverage: Rules, Endowments, Zap and their interfaces read in full; the live Questions, WorkOrders and CrowdPots sources could not be reached","path":"src/interfaces/external/Mainnet.sol","line":36,"description":"Read line by line: src/rules/Rules.sol, src/rules/Zap.sol, src/endowments/Endowments.sol, src/interfaces/IWorkOrders.sol, IQuestions.sol, ICrowdPots.sol, src/rules/v4/IPoolManagerLike.sol, src/interfaces/external/Mainnet.sol, docs/LAUNCH.md and docs/THREATS.md, plus the stand-ins MockDesk, MockStakedIMD, MockPots, MockCrowdPots, MockIMD and the Endowments/Rules test bases. Every external and public state-changing function was walked against the Pashov SOP (access, value in and out, reentrancy, rounding, time, external trust): no loss of funds, no payment to a party other than the one the creator or caller fixed, no broken accounting, no reentrancy path past the transient locks, no overflow, and no forbidden construct (no owner, proxy, initializer, mint, pause, DELEGATECALL, CALLCODE or SELFDESTRUCT; constructors make no external call). forge build and the full default suite plus the fork suite (public RPC reachable) passed: 442 tests, 0 failures. The manifest in docs/LAUNCH.md has only kind/contracts/notes, 2/4/4 string arguments in constructor order, no $owner and no $contract reference, and its six addresses match Mainnet.sol and the brief. Not reached: the real sources of Questions (launch 1531) and WorkOrders/CrowdPots (launch 1573) behind the frozen interfaces, since the sibling repositories integration/ builds against are not in this checkout; conclusions about those three rest on the interface comments, the stand-ins that mirror them and the fork tests that call the live contracts. The invariant suites (test/**/invariant/**) were not re-run within the time budget; the project's own run is the evidence for them.","reproduction":"Not a defect. Commands run: forge build; forge test --no-match-path 'test/**/invariant/**' (442 passed, including the fork suites); a scratch test for the two findings above.","snippet":"    address internal constant QUESTIONS = 0xEECE4628ceb091C13393fc64056Be8b8501fD3F6;","citation":"resolved","blocking":false}],"coverage":null}]}