{"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":"f6219aa5-8cf9-409f-b099-8244a20639a4","kind":"audit","nodes":[{"acceptedSubmissionHash":"4d5b37cc9049dee4316aaf42481d52f8e1f1f18db3e17da272970ccb2ba83941","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"354ca509d2cee370124d651b1dc98881f1fb0a91cd35bbf2cd5795129d7da1be","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"f48efc79a11f8b8929d4ec56751ce35d81de1119790bd0440a5126df678ea6f3","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"d67413798925f4abc5734a9be649e5f4cb622dc1cfcbe6c30cc836bc51857565","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"c9670daa0a2b5ec4c6a5a5119a67dbf9eacd0108a1385cb47ad3f5e6ca8cee86","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Audit governance and the Treasury: src/Parameters.sol, src/Governed.sol, src/Treasury.sol, src/TreasuryFactory.sol, src/WorkOracleFactory.sol and src/SwarmWorkOracle.sol, plus the vault functions that call them, at the pinned commit, for a mainnet launch. Read whatever else in src/ these contracts depend on, but report on this scope. Four audit rounds and their fixes are already in (docs/AUDIT-*.md; the newest are docs/AUDIT-FINAL-PANEL-VAULT/GOVERNANCE/ORACLE-2026-10-07.md, fixed in 8756817: git show 8756817). This is the last sweep before the deployment commit is frozen, so it audits the code as it will deploy; a finding of an earlier round counts only if its fix regressed or left a gap. Spend turns on breaking the newest fixes first.\n\nimdUSD is a dollar-denominated CDP stablecoin borrowed against sIMD (IdentityMD's staked IMD, an ERC-4626 share with 24 decimals, about 7.95 IMD each). Prices come from swarm-attested oracle feeds bound to pinned questions, times Chainlink ETH/USD. Everything about the deployment is in src/DeploymentConfig.sol and docs/MAINNET-RUNBOOK.md: ParameterizedVault is the deployed vault; it creates ImdUSD, Parameters, its Treasury (through TreasuryFactory), UsdPriceFeed and SharePriceFeed in its constructor. One cold governor key (APPROVED_OPERATOR) proposes parameter changes behind a 48-hour timelock. Collateral pricing is per 1e18 raw units throughout. IMD's only market is a full-range Uniswap v4 pool, about $2.3M a side with a 1% fee; docs/PARAMETERS-2026-10-05.md has the numbers every economic parameter was chosen from.\n\nAnswer each numbered question, including the ones where nothing is wrong:\n1. Timelock and bounds: any change applied sooner than 48 hours, outside its bounds, or by anyone other than the documented route; any proposal blocked indefinitely or applied at a chosen moment to harm borrowers.\n2. Treasury exits, enumerated and bounded: withdraw, withdrawNative, payStream, fundOracle (now plain IMD first unless IMD is a listed reserve asset, then sIMD unwrapped; topped up to the daily budget), redeemIMD, cover. Day boundaries, rounding, rate changes, and the accounting (sync, lastSynced, totalReceived) across the plain-IMD path.\n3. Reserve valuation with the bounded reads (_boundedCall copies at most two words): can a listed feed or token still make reserveValueUsd, earnLine, backingPerUnit or cash revert or misvalue (gas, malformed words, a token that reverts on balanceOf, decimals)? Is the memory use of the assembly sound?\n4. The work oracle: proposeWorkOracle at wage 0, successors built directly (not through WorkOracleFactory.create), predecessor() after a first mint; SwarmWorkOracle.claim now refusing once superseded (probing vault.oracle()). Can rights be claimed, consumed or stranded wrongly across a replacement and a wage cycle, and does the probe behave for a vault with no oracle()?\n5. The governor's minting power, stated in Parameters as a trust assumption (reserve listing against any shape-valid feed, plus a replacement oracle and a wage): is the statement complete and are the bounds (48 hours each, MAX_RESERVE_VALUE per asset, earn closed at wage 0) as described?\n6. Day one: the asker seeded with IMD at deploy, the keeper's fallback, fundOracle's sources. Any state in which the oracle path is dead and nothing in docs/MAINNET-RUNBOOK.md section 7 revives it?\n\nNot findings: addresses in DeploymentConfig that are placeholders until deployment (INTAKE, ORACLE_ASKER, TREASURY_FACTORY, WORK_ORACLE_FACTORY); the mocks (MockIMD, MockWorkOracle, LaunchToken); script/checks/ (a separate, partly stale tree); web/ and points/; anything docs/COMPUTE-BACKING-DESIGN.md describes as future work; and findings of the earlier audits in docs/AUDIT-*.md and docs/INTERNAL-AUDIT-2026-10-04.md, unless the fix regressed. A constant set to a deliberate economic value is not a finding; an arithmetic or ordering error in how it is used is.\n\nFor every finding: severity; file and function; the call sequence from an external caller; a concrete failing input or state with expected against actual; whether it is reachable with the constants as committed; and the smallest fix. Also report every place a comment or NatSpec claims a property the code does not have, and say which contracts you read in full and which you could not reach.","parentJobId":null,"planHash":"c7542f1815ef97a3e24afdcae6a3a0fa7bf79064d32a907ce09d227b371e0c2b","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"f6219aa5-8cf9-409f-b099-8244a20639a4","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":"51020","feedbackHash":"d837663c530714fe916edd3abd2c61cbd2223f29e905b468cf5a2150bedaeb75","nodeKey":"audit_economics","submissionHash":"4d5b37cc9049dee4316aaf42481d52f8e1f1f18db3e17da272970ccb2ba83941","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51515","feedbackHash":"b8957f02f613d10d1ec03856c19d518843dc0014887780710a0383f8e21b0f60","nodeKey":"audit_flow","submissionHash":"354ca509d2cee370124d651b1dc98881f1fb0a91cd35bbf2cd5795129d7da1be","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51343","feedbackHash":"895653d60bd3560f6abbc4d5cc060e3cca1ee6d5c7cbe9430f317add6b1d3b05","nodeKey":"audit_judge","submissionHash":"f48efc79a11f8b8929d4ec56751ce35d81de1119790bd0440a5126df678ea6f3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51557","feedbackHash":"3b368af92367937a42ac0a712962f8e78da4ed1c7d41b1f926448b349b948ab6","nodeKey":"audit_math","submissionHash":"d67413798925f4abc5734a9be649e5f4cb622dc1cfcbe6c30cc836bc51857565","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51445","feedbackHash":"60e536ee5b0b2d96d0954a8e15dd991eb6caff379b5c10c5ac1a4b90418081ad","nodeKey":"audit_permissions","submissionHash":"c9670daa0a2b5ec4c6a5a5119a67dbf9eacd0108a1385cb47ad3f5e6ca8cee86","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"aeb5acbfeed8f484dc98e06133b81bdc24e2cc4334ce4f97666f12b9db2a3499","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ca2eb76d92edd79d","findings":[],"hash":"1e0650805cc184628c71883372681c724f03e370752e08f00cf4171de670430d","nodeId":"0161df7a-75c9-445d-90c0-16d9aea627bb","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":3,"wallClockMs":223794}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"ea89e16822824c6f","findings":[{"citation":"resolved","description":"Q3 (gas), breaking the newest fix. The final panel's low (governance #3) was fixed by copying at most two words of returndata in `_boundedCall`, and the NatSpec at Treasury.sol:206-208 ('it never makes this view revert'), 219-221 and 267-272 now present the reserve reads as isolated from a misbehaving listed source. The copy is bounded; the GAS is not: `staticcall(gas(), ...)` hands the callee 63/64 of whatever the Treasury has, and a feed or token whose `isStale()` / `latestValue()` / `balanceOf()` spins until it runs out keeps all of it. EIP-150 leaves each caller 1/64 of its own gas, so after one hostile read the Treasury has 1/64 of what the vault gave it and the vault 1/64 of what the user gave it. Consequences, measured: (a) one hostile listed feed: `cash(1e18, 0, candidate)` reverts out of gas at 1M, 2M, 3M and 4M gas and succeeds at 6M, i.e. every `earn`, `cash` and `backingPerUnit` read costs the caller about 5M gas of burned work for as long as the feed is listed (a 48-hour delisting); (b) three hostile listed sources: `reserveValueUsd()`, `earnLine()`, `backingPerUnit()`, `earn` and `cash` all revert at 30,000,000 gas (the Treasury keeps 30M/64^3 = 114 gas after the third read), so the work ceiling and the whole redemption channel are shut until three sequential delistings mature (one slot: 144 hours). Reach: it needs a governor-listed source that misbehaves AFTER listing (an upgradeable or owner-controlled feed or token; `validateReserveAsset` probes shape, and a probe that burned gas would be refused at proposal), 48 hours visible each; the mainnet register (sIMD through the vault's own immutable SharePriceFeed) cannot do it, so not reachable with the constants and the planned listing as committed, and inside the governor trust the file states. Liveness only; nothing is mispriced because a failed read counts for zero. Comments claiming the property the code does not have: Treasury.sol:206-208, 219-221 and 267-272. Smallest fix: cap the gas forwarded per read, e.g. `staticcall(200000, target, ...)` (SharePriceFeed -> UsdPriceFeed -> Chainlink is well under 100k), so a hostile source can burn at most 200k per read and the sum completes at any ordinary gas limit; and reword 206-208 to say what is actually promised.","line":277,"path":"src/Treasury.sol","reproduction":"test/scratch/GasBurnThree.t.sol (PASSES as a demonstration; logs the outcomes). WorkBackingFixture (OpenWorkVault over MockIMD at $1, NHI 0.85, empty register). GasBurnFeed answers (1e18, now) and isStale()==false until armed, then every read loops `x = keccak256(x)` forever. (1) `_fundReserve(10e18)`; list an 18-decimal token against the GasBurnFeed at haircut 10000 through proposeReserveAsset + 48h + applyPending; mint 10e18 of it to the Treasury; `_openDebt(100e18)`; arm the feed. BORROWER calls cash(1e18, 0, BORROWER) with 1,000,000 / 2,000,000 / 3,000,000 / 4,000,000 gas: EXPECTED (NatSpec 206-208, 'counts for nothing') success at ordinary gas; ACTUAL all four revert (out of gas); at 6,000,000 it succeeds. (2) Three such tokens/feeds listed and armed: reserveValueUsd{gas:30_000_000}(), earnLine{gas:30M}(), backingPerUnit{gas:30M}(), earn(1e18){gas:30M} by WORKER and cash(1e18,0,BORROWER){gas:30M} ALL revert (logged 0), where the promise is a sum that counts the three for nothing and returns 10e18. Control in test/scratch/GasBurnFeed.t.sol: with ONE armed feed reserveValueUsd{gas:16M} returns 10e18 and earn succeeds at 5M and 30M but not at 1M.","severity":"low","snippet":"            success := staticcall(gas(), target, add(data, 32), mload(data), out, 64)","title":"Treasury._boundedCall forwards all gas to a listed feed or token, so one hostile source puts a ~5M-gas floor under every earn, cash and backingPerUnit and three make reserveValueUsd revert at any gas "},{"citation":"resolved","description":"Q4 (rights stranded wrongly across a replacement and a wage cycle). `_validate` requires a successor to name the current oracle as its `predecessor` only once `vault.totalEarned() != 0`; the NatSpec at Parameters.sol:252-255 says 'before that, there is nothing to carry over', and ParameterizedVault.sol:122-123 says 'Rights claimed under a wage are kept, and spendable again the moment a wage is set'. Neither holds once a claim has been made without a mint. SwarmWorkOracle prices rights AT CLAIM (`creditedRights`, line 170-172) and a claim needs only a nonzero wage and an accepted root; `earn` needs the work ceiling to have room, which at launch (empty register, little debt) it often will not, so 'claimed, not yet minted' is the ordinary state of an agent's rights. The replacement route then REQUIRES governance to zero the wage (`WorkMintingOn`) and, before any mint, admits a fresh `SwarmWorkOracle` with no `predecessor()`. After it is applied the vault reads the successor, where the agent has nothing; the old oracle refuses `claim` (`NotTheVaultsOracle`) and only the vault may `consumeRights`, so the rights priced under the old wage are unconsumable forever. In the successor `creditedTasks` starts at zero, so the SAME cumulative tally is claimable again, at whatever wage governance sets next, by whoever controls the agent NOW. The agent therefore loses the price their work was credited at (1.0 to 0.1 imdUSD per task in the reproduction, 500 to 50 imdUSD), and if the identity NFT changed hands in between the whole credit for work done under the previous controller moves to the buyer, which contradicts SwarmWorkOracle.claim's own split rule (lines 146-149: 'the previous one keeps what they already claimed'). No double CONSUMPTION is possible (at most one oracle is read, and a return to the old one is itself a WorkOracle proposal), so this is not a bypass; it is a governance-visible loss and a NatSpec claim without the property. Reachable with the committed code after one ordinary wage cycle (wage set, claims made, wage zeroed, fresh SwarmWorkOracle proposed and applied), never at launch (WAGE_WAD = 0, no claims). Smallest fix: gate the predecessor requirement on whether the current oracle holds any credited rights rather than on `totalEarned` (e.g. a `totalCredited` counter on SwarmWorkOracle read by `_validate`, with the mock faucet's absence of it treated as zero), or have `_validate` refuse a successor without `predecessor()` whenever `vault.oracle()` answers a nonzero `totalCredited()`; and reword Parameters.sol:253-255 and ParameterizedVault.sol:122-123 ('kept' is true only while the same oracle stays the vault's).","line":400,"path":"src/Parameters.sol","reproduction":"test/scratch/ReplacementStrandsClaims.t.sol test_claimedRightsAreRecreditedAtTheNewWageToTheNewController (PASSES as a demonstration). ParameterizedVault built with WORK_ORACLE_SENTINEL over a SeedableWorkOracle (production SwarmWorkOracle plus a seeding door), an ERC-8004 adapter etched at ERC8004_ADAPTER answering isController from a mapping. Governance: proposeWage(1e18), +48h, applyPending. Root over leaves (7, 500, 500) and (8, 1, 1) seeded and recorded; CTRL controls agent 7 and claims: work.mintingRights(CTRL) == 500e18. Governance: proposeWage(0), +48h, apply; `new SwarmWorkOracle(address(vault), 1 days)` proposed through proposeWorkOracle (totalEarned == 0, so accepted), +48h, apply: vault.oracle() == successor. proposeWage(0.1e18), +48h, apply. The NFT is sold: adapter now says BUYER controls agent 7. BUYER claims the same leaf in the successor. EXPECTED per ParameterizedVault.sol:122-123 and Parameters.sol:253-255: CTRL's 500e18 of rights are spendable through earn, and there was nothing to carry over. ACTUAL: successor.mintingRights(CTRL) == 0 and successor.claim by BUYER credits 50e18 (500 tasks at the new wage) to BUYER, while work.mintingRights(CTRL) == 500e18 sits in an oracle the vault no longer reads and that refuses claims (asserted).","severity":"low","snippet":"            bool minted = vault.totalEarned() != 0;","title":"Parameters.proposeWorkOracle lets a replacement without predecessor() in while rights claimed under an earlier wage are still outstanding, so those priced rights are stranded in the superseded oracle "},{"citation":"resolved","description":"Q2 and Q6, a gap left by the final panel's fix (governance low #2: spend plain IMD first). The plain IMD and the share unwrap happen in ONE call: `plain` is sent only together with `fromShares`, and `_withdrawUnderlying` calls the share vault's `withdraw`, which sIMD refuses (`SameBlockRedeem`) for shares that reached the Treasury in the current block. So whenever the Treasury's plain IMD is below the day's remaining budget and it also holds shares that arrived this block, the revert of the share leg takes the plain leg with it and the asker gets NOTHING that block, where the fix's promise (NatSpec 487-488, runbook 7.4 'spends the Treasury's plain IMD first') is that plain IMD funds the oracle. The self-inflicted case (a liquidation's protocol cut lands in the same block as a keeper's fundOracle) clears a block later, as the NatSpec at 488-490 says; the griefing case accepted as D5 (one raw sIMD transferred to the Treasury every block, inheriting the sender's hold) now denies the plain IMD revenue too, which D5's acceptance ('a later block succeeds') did not weigh. Bounded, keyless revival exists (the keeper fallback and askPaid), so info rather than low. CAVEAT: the hold is modelled from the repository's own fork notes (test/SharePriceFeedFork.t.sol:170-176, COMPUTE-BACKING-DESIGN.md:537-548), not re-verified against StakedIMD (no fork available here). Smallest fix: unwrap through a guarded path so the plain leg lands regardless, e.g. `try this.unwrapForOracle(fromShares) {} catch { fromShares = 0; }` (an external self-call wrapping `_withdrawUnderlying`, `msg.sender == address(this)`), computing `sent` and `oracleSpent` from what actually went out; or send `plain` first and return early with `sent = plain` when `IShareVault(token).lastDepositBlock(address(this)) == block.number`.","line":524,"path":"src/Treasury.sol","reproduction":"test/scratch/FundOracleHold.t.sol (PASSES as a demonstration). HeldShareVault: an ERC-4626-shaped 24-decimal share (rate 1.25e12) that records lastDepositBlock[receiver] on deposit, propagates the later of the two holds on transfer and reverts SameBlockRedeem in withdraw when lastDepositBlock[owner] >= block.number. ParameterizedVault over it; ORACLE_ASKER etched with code; default budget 15e18. (1) 10e18 plain IMD minted to the Treasury (below the budget) and 50e18 of IMD deposited as shares TO the Treasury in the same block: fundOracle() EXPECTED to send at least the 10e18 plain IMD; ACTUAL reverts SameBlockRedeem and imd.balanceOf(ORACLE_ASKER) == 0 (asserted). (2) Shares deposited a block earlier; GRIEFER deposits 1e18 IMD and transfers one raw share to the Treasury; fundOracle() reverts SameBlockRedeem the same way; one quiet block later it sends 15e18 (10 plain + 5 unwrapped).","severity":"info","snippet":"        if (plain < want) {","title":"fundOracle's new plain-IMD path is lost whenever the share unwrap it falls through to reverts under sIMD's same-block hold: a liquidation cut (or the accepted D5 dust transfer) in the same block makes"},{"citation":"resolved","description":"Q5, is the statement complete and are the bounds as described. The mechanisms are stated correctly (reserve listing against any shape-valid source; a replacement oracle while the wage is zero; earn refused at wage 0; 48 hours each), and the code matches: `_validate` enforces every one. Two of the bounds are described more strongly than they are. (1) 'after 96 hours of public proposals' holds only for the oracle + wage pair, which mints against the ratio term (backedDebt x earnMat / 10000, at most 25% of the lagged collateral-backed debt). The 'together' route with a reserve listing is three proposals and Governed holds one at a time (`ProposalPending`), so it is 144 hours minimum; if a wage is already live a fourth step (wage to zero) makes it 192. (2) 'the per-asset cap' is Treasury.MAX_RESERVE_VALUE = 1e36 in 1e18-scaled USD, i.e. $1e18 per listed asset, with `_reserveAssets` unbounded in length: one listing alone authorises a trillion times LINE ($1e6) of work minting, and `earn` reads no `line`. Stated as a bound it reads as a limit on the damage; it is a limit on arithmetic overflow (its NatSpec at Treasury.sol:63 says so). (3) Not mentioned: the same listing raises the MEASURED backing, `_redemptionReserveBacking`'s `others` term and so `backingPerUnit`, up to par, so redemptions are paid at par out of real sIMD against an under-backed supply, which transfers from the remaining holders to redeemers on top of the dilution the sentence names. No code change is implied by any of this (the design accepts the cold-key governor); the correction is to the statement: 'after 96 hours (oracle and wage) for up to a quarter of the lagged debt, or 144 hours with a listing, bounded per asset only at $1e18'.","line":58,"path":"src/Parameters.sol","reproduction":"Read-only against the committed constants and Governed. Governed._propose reverts ProposalPending while pendingEta != 0 and TIMELOCK is 48 hours, so proposeReserveAsset -> applyPending -> proposeWorkOracle -> applyPending -> proposeWage -> applyPending cannot complete before 3 x 48 = 144 hours; the sentence says 96. Treasury.MAX_RESERVE_VALUE = 1e36 (1e18-scaled USD) and DeploymentConfig.LINE = 1_000_000e18: 1e36 / 1e24 = 1e12. The path itself is the one the final panel reproduced (docs/AUDIT-FINAL-PANEL-GOVERNANCE-2026-10-07.md finding 5): a junk token with 1e18 units in the Treasury listed against a feed answering 1e30 gives reserveValueUsd() == earnLine() == 1e30, and with a governor-controlled oracle and a nonzero wage `earn(1_000_000_000e18)` succeeds with totalDebt == 0; the same 1e30 flows into ParameterizedVault._redemptionReserveBacking (line 178) and backingPerUnit() reads 1e18 whatever the real reserve is.","severity":"info","snippet":"/// Together, after 96 hours of public proposals, that is minting with no collateral and no attested","title":"Parameters' governor-can-mint trust statement understates its own bounds: the three-step route takes 144 hours, not 96 (one slot), and the 'per-asset cap' is MAX_RESERVE_VALUE = $1e18 per listing with"}],"hash":"354ca509d2cee370124d651b1dc98881f1fb0a91cd35bbf2cd5795129d7da1be","nodeId":"1733bd45-5f8e-409d-b815-01e04c5ad12d","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds four findings, every snippet and line verified against the tree. No tracked file was changed; only `.imd-findings.json` and four scratch tests under `test/scratch/` were written. The in-scope suites still pass (126 tests across Treasury, OracleBudget, WorkOracleGovernance, Parameters, SwarmWorkOracle, ReserveValuation, LaggedBacking).\n\n## Findings\n\n| Sev | Where | Finding |\n|---|---|---|\n| low | `src/Treasury.sol:277` `_boundedCall` | The returndata copy is bounded but the gas is not. One hostile listed feed puts a ~5M-gas floor under every `cash`/`earn` (reverts at 4M, succeeds at 6M); three hostile sources make `reserveValueUsd`, `earnLine`, `backingPerUnit`, `earn` and `cash` revert at 30M gas for three sequential 48-hour delistings. Needs a governor-listed source that turns hostile after listing. Fix: cap the gas per read. |\n| low | `src/Parameters.sol:400` `_validate(WorkOracle)` | The predecessor rule keys on `totalEarned`, so rights claimed under a wage but never minted are stranded when a fresh `SwarmWorkOracle` replaces the oracle, and the same tasks are re-credited in the successor at the new wage, to whoever controls the agent then (500 to 50 imdUSD, moved to the NFT buyer in the reproduction). Contradicts \"nothing to carry over\" and \"rights claimed under a wage are kept\". |\n| info | `src/Treasury.sol:524` `fundOracle` | The new plain-IMD leg is sent in the same call as the share unwrap, so a same-block sIMD arrival (liquidation cut, or the accepted D5 dust transfer) reverts the whole call and the plain IMD is not sent either. Hold modelled from the repo's fork notes, not re-verified. |\n| info | `src/Parameters.sol:58` trust statement | The three-step mint route is 144 hours, not 96 (one slot); the \"per-asset cap\" is $1e18 per listing with unbounded listings, a trillion times LINE; the listing also inflates the measured `backingPerUnit`. |\n\n## Answers to the numbered questions\n\n1. **Timelock and bounds.** Clean. One slot, `onlyGovernor` on propose and cancel, `_validate` at both ends, every bound a constant. With `earn` refused at wage 0 nothing a third party can do flips `totalEarned` during a pending `proposeWorkOracle`, so the panel's griefing is closed. The no-expiry-after-eta note in `Governed.sol:73-79` remains the accepted position.\n2. **Treasury exits.** All six are enumerated and bounded as documented. Day boundaries, rate changes and rounding hold. The plain-IMD path's accounting (`_withdraw`: credit arrivals, move the baseline, re-read) is correct and `sync` agrees afterwards. The only gap is the same-block hold dragging the plain leg down (info).\n3. **Reserve valuation.** Malformed words, short returns, reverting `balanceOf`, decimals up to 77 and the overflow and $1e18 guards are all handled and count for zero. The `memory-safe` assembly is sound: it uses unallocated memory past the free pointer as scratch and reads both words before Solidity allocates again. Gas is the one dimension the fix left open (low).\n4. **Work oracle.** `proposeWorkOracle` at wage 0, direct construction, the `predecessor` rule after a first mint and the superseded-oracle probe all behave; a vault with no `oracle()` reads as \"still mine\". No double consumption is possible. Rights can be stranded across a pre-first-mint replacement (low).\n5. **Governor's minting power.** Mechanisms and the 48-hour steps are stated correctly; the timing and the per-asset cap are stated more strongly than they are, and the inflated measured backing is omitted (info).\n6. **Day one.** No dead state without a revival: the keeper fallback and `askPaid` cover an empty Treasury, listed IMD is a documented 48-hour delisting, and the hold case clears a block later.\n\n## NatSpec claims without the property\n\n`Treasury.sol:206-208` and `267-272` (never reverts), `Parameters.sol:58` (96 hours) and `253-255` (nothing to carry over), `ParameterizedVault.sol:122-123` (claimed rights kept), `Treasury.sol:488-490` (t","treeHash":null,"usage":{"cachedInputTokens":3010952,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":69070,"runtime":"claude","turns":56,"wallClockMs":1167862}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"a31e321b410aaa02","findings":[{"citation":"resolved","description":"Gap in the newest fix (final panel audit, governance, low #3). _boundedCall still forwards gas(), so a listed price source (or listed token) whose isStale/latestValue/balanceOf consumes all the gas it is given (an unbounded loop, INVALID 0xfe, a broken upgrade) takes 63/64 of the caller's remaining gas on every read. reserveValueUsd loops over every listed asset, so k such assets leave the caller 1/64^k of its gas. That contradicts the NatSpec on reserveValueUsd ('it never makes this view revert') and on _boundedCall (which says the reads can no longer revert earnLine, earn, backingPerUnit and every cash). The fail-safe the register promises, that a dead source counts for nothing until a delisting matures 48 hours later, holds for a source that reverts or returns too much data, but not for one that burns gas. Call sequence: any caller -> ParameterizedVault.earn / cash / backingPerUnit -> earnLine or _redemptionReserveBacking -> reserveValue -> Treasury.reserveValueUsd -> reserveValueOf -> _reservePrice -> _readBool -> _boundedCall. Reachability with the constants as committed: the launch register holds only sIMD against the vault's own SharePriceFeed, which does not burn gas. The problem appears only after governance lists third-party (for example upgradeable) sources and one of them later starts consuming its gas, the same trust level as the returndata-bomb finding the fix addressed. Smallest fix: forward a fixed stipend instead of gas(), for example staticcall(RESERVE_READ_GAS, ...) with RESERVE_READ_GAS around 100k, and require gasleft() > RESERVE_READ_GAS * 64 / 63 before the call so a caller cannot starve an honest read on purpose.","line":277,"path":"src/Treasury.sol","reproduction":"ParameterizedVault over MockIMD collateral, with TreasuryFactory etched at TREASURY_FACTORY. Three 18-decimal tokens each hold 1e18 at the Treasury. Each is listed with haircut 10000 against its own feed, which returns latestValue() = (1e18, now) and isStale() = false (APPROVED_OPERATOR proposeReserveAsset, +48h, applyPending, three times). Healthy state: reserveValueUsd() == 3e18, and vault.earnLine() succeeds with 1,000,000 gas. Then each feed's isStale() is switched to `while (true) {}`. Expected (NatSpec): earnLine() and reserveValueUsd() complete and the three assets count for zero. Actual: call{gas: 30_000_000} to vault.earnLine() returns ok == false, and call{gas: 30_000_000} to treasury.reserveValueUsd() also returns ok == false after using all 30,004,815 gas. The first isStale call burns 29.0M, the second 0.45M, and the third finds about 7k left. So earn, cash and backingPerUnit cannot run within a 30M block until all three delistings mature, 48 hours each and one at a time through the single proposal slot. With only two such feeds the view itself still completes but leaves about 7k gas, which is not enough for the rest of earn or cash. Verified in test/scratch/Probe.t.sol test_gasBurningFeeds (forge test --match-path).","severity":"low","snippet":"            success := staticcall(gas(), target, add(data, 32), mload(data), out, 64)","title":"_boundedCall bounds the returndata copy but not the gas: listed feeds that consume their gas still revert reserveValueUsd, earnLine, earn and cash"},{"citation":"resolved","description":"Gap in the newest fix (final panel audit, governance, low #2: 'fundOracle spends plain IMD first'). When plain IMD covers only part of the top-up (plain < want), the remainder is unwrapped from sIMD in the same call with no fallback. sIMD's withdraw reverts SameBlockRedeem in any block where the Treasury received shares, which happens with a liquidation cut in that block (the case the NatSpec itself documents) or with a 1-raw-unit dust transfer from anyone (accepted residual D5). In both cases the revert also undoes the plain-IMD transfer, which needs no unwrap. So the plain-IMD revenue that runbook section 7.4 says 'takes over' from the keeper is still blockable by the share leg: the asker is not funded from the Treasury in any block where the share leg is held, even when the Treasury holds enough plain IMD to cover most of the budget. Call sequence: anyone -> Treasury.fundOracle -> _withdraw(imd, ORACLE_ASKER, plain), then _withdrawUnderlying -> IShareVault.withdraw, which reverts. Reachability: once ORACLE_ASKER has code, with the committed ORACLE_BUDGET_PER_DAY = 15 IMD and any plain-IMD balance below the day's room. A later block succeeds, so this is griefing or delay, not loss. Smallest fix: make the share leg best-effort. Wrap IShareVault(token).withdraw in try/catch inside _withdrawUnderlying, or call it from fundOracle with try. On failure, restore the lastSynced baseline, count only `plain` in oracleSpent and `sent`, and return. Alternatively, skip the share leg when the Treasury received shares this block.","line":533,"path":"src/Treasury.sol","reproduction":"ParameterizedVault whose collateral is a 24-decimal share of IMD modelling sIMD's documented hold: any incoming transfer sets lastIn[to] = block.number, and withdraw reverts 'SameBlockRedeem' while lastIn[owner] == block.number. ORACLE_ASKER is etched with code and the default oracleBudget is 15e18. The Treasury holds 5e18 plain IMD (launch-pool fees) and 50e18 IMD worth of shares deposited in an earlier block. In block N, anyone deposits 1 wei of IMD into the share and transfers 1 raw share unit to the Treasury, or a bite pays the protocol cut there. Then anyone calls treasury.fundOracle(): want = 15e18, plain = 5e18, fromShares = 10e18. Expected (NatSpec and runbook 7.4: plain IMD is spent first): at least the 5e18 plain IMD reaches the asker. Actual: the call reverts 'SameBlockRedeem' and imd.balanceOf(ORACLE_ASKER) == 0. Verified in test/scratch/Probe.t.sol test_plainIMDBlockedByShareLeg.","severity":"low","snippet":"            if (share) _withdrawUnderlying(IERC20(token), fromShares);","title":"fundOracle's plain-IMD leg is all-or-nothing with the sIMD unwrap: any revert on the share leg also keeps the Treasury's plain IMD from the asker"},{"citation":"resolved","description":"The TRUST ASSUMPTION block in Parameters (lines 54-62) describes the governor's power as minting: a listing sets the reserve term of earnLine, and minting takes a replacement oracle plus a wage, '96 hours of public proposals', with 'the oracle step alone mints nothing'. But every listed non-collateral asset also feeds the redemption backing. ParameterizedVault._redemptionReserveBacking computes others = reserveValue() - reserveValueOf(gem) (src/ParameterizedVault.sol:178). CDPVault._backingPerUnit adds that to secured collateral, and cash pays amount * backingPerUnit * (1 - fee) / price. So a single proposeReserveAsset(token, feed, 10000) against a feed the governor controls (refused only if it is the collateral or imdUSD, or malformed) raises backingPerUnit to the 1e18 cap after 48 hours, with no oracle or wage step. After that, any imdUSD holder (including one who bought below peg) redeems at par minus the fee, paid in real sIMD from the Treasury and from candidate positions, and the loss lands on the remaining holders. This is a governed power by design, but the stated trust assumption omits it and implies a 96-hour, mint-only path. The ParameterizedVault header ('See Parameters for what that lets the governor do') defers to the same incomplete text. Fix: documentation only. State in the Parameters trust assumption, and in runbook section 3, that a listing alone also sets the redemption payout up to par, after 48 hours. If that power is not intended, exclude registered non-collateral assets from the redemption backing instead (they never leave through redeemIMD).","line":56,"path":"src/Parameters.sol","reproduction":"WorkBackingFixture (ParameterizedVault over MockIMD at $1, earn open, lag off). _openDebt(1000e18) (2000 IMD locked), earn 250e18 work-minted, warm the lag one day, then set the vault price to $0.50. backingPerUnit() == 0.8e18 (secured 1000 USD over supply 1250). APPROVED_OPERATOR proposeReserveAsset(junk, fakeFeed, 10000), where junk is a fresh 18-decimal token with 1e18 at the Treasury and fakeFeed answers latestValue() = (1e30, now) and isStale() = false. +48h, applyPending by anyone. No oracle or wage change. Expected per the trust statement: no change to what an existing holder is paid. Actual: backingPerUnit() == 1e18, so cash(amount) now pays (1 - fee) of par in sIMD instead of 0.8 x (1 - fee). Verified in test/scratch/Listing.t.sol test_listingAloneLiftsRedemptionPayout (backing before 800000000000000000, after 1000000000000000000).","severity":"low","snippet":"/// in shape (`proposeReserveAsset`), which sets the reserve term of `earnLine` up to MAX_RESERVE_VALUE per","title":"The governor trust statement covers minting only: one 48-hour reserve listing against any shape-valid feed also lifts backingPerUnit, and so the redemption payout, to par while real backing is below i"}],"hash":"4d5b37cc9049dee4316aaf42481d52f8e1f1f18db3e17da272970ccb2ba83941","nodeId":"d5683b3c-0b1a-4d84-acb9-199548223c82","outcome":"completed","summary":"I found three low-severity issues and no critical, high or medium ones. All three are in `.imd-findings.json`, each reproduced with a scratch Foundry test that I ran and then deleted. No source files were changed.\n\n## Findings\n\n**1. [low] Reserve reads cap the data copied back, not the gas spent** (`src/Treasury.sol:277`, `_boundedCall`)\n- **What's wrong:** the newest fix stops a listed feed from returning megabytes of data. But each read still hands the feed all remaining gas, and a feed that burns it takes 63/64 of the caller's gas per read.\n- **Failing state:** three listed assets whose `isStale()` loops forever. `earnLine()` and `reserveValueUsd()` both revert with 30M gas; the same setup with healthy feeds completes within 1M. `earn`, `cash` and `backingPerUnit` stay blocked until three delistings mature, 48 hours each, one at a time.\n- **Reachable?** Not with the launch register (only sIMD, priced by the vault's own feed). Only after governance lists outside feeds and one of them goes bad.\n- **Smallest fix:** give each read a fixed gas allowance (about 100k), and check enough gas is left before calling so a caller can't starve an honest read on purpose.\n\n**2. [low] `fundOracle`'s plain-IMD payment fails whenever the sIMD unwrap fails** (`src/Treasury.sol:533`)\n- **What's wrong:** if the Treasury's plain IMD covers only part of the top-up, the rest is unwrapped from sIMD in the same transaction. When that unwrap hits sIMD's same-block hold, it reverts and takes the plain-IMD transfer down with it.\n- **Failing state:** the Treasury holds 5 IMD plain plus shares; someone sends 1 raw sIMD unit in that block (or a liquidation cut lands). `fundOracle()` reverts `SameBlockRedeem`, and the asker gets nothing, not even the 5 IMD.\n- **Impact:** a delay that anyone can trigger (a later block succeeds), not a loss. It weakens the runbook §7.4 claim that plain-IMD revenue takes over from the keeper.\n- **Smallest fix:** make the sIMD unwrap optional (try/catch it); if it fails, still send the plain IMD and count only that toward the day's budget.\n\n**3. [low] The governor trust statement covers minting but not redemption** (`src/Parameters.sol:56`)\n- **What's wrong:** the statement says the governor's power is minting, after 96 hours (two proposals). But any listed non-collateral asset also counts toward `backingPerUnit` (`ParameterizedVault.sol:178`), which sets what `cash` pays out.\n- **Failing state:** real backing is 0.8. One listing against a feed the governor controls, applied after 48 hours with no oracle or wage change, lifts `backingPerUnit` from 0.8e18 to 1e18. Redeemers then get par minus the fee in real sIMD, and the remaining holders take the loss.\n- **Smallest fix:** say so in the trust statement and runbook §3. Or, if the power isn't intended, leave listed non-collateral assets out of the redemption backing.\n\n## The six questions\n\n1. **Timelock and bounds:** clean. Nothing applies before 48 hours, every bound is checked again when a change is applied, and only `APPROVED_OPERATOR` can propose or cancel. No outsider can block a pending change for good: the oracle replacement's checks can't move while the wage is 0, and a delisting can't be refused. The open-ended window after the 48 hours is the earlier accepted note.\n2. **Treasury exits:** `withdraw`, `withdrawNative`, `payStream`, `redeemIMD` and `cover` are bounded as documented, including UTC day resets, mid-day budget or rate cuts, and bad debt being protected. The `sync`, `lastSynced` and `totalReceived` bookkeeping is right on both the plain-IMD and the sIMD path. The one gap is finding 2.\n3. **Reserve valuation:** malformed answers, short data, oversized data, a token that reverts on `balanceOf`, and decimals above 77 are all handled. The assembly's memory use is sound: it writes past the free-memory pointer without moving it, which is allowed, and leftover memory is caught by the size checks. The one gap is gas (finding 1).\n4. **Work oracle:** no rights can be spe","treeHash":null,"usage":{"cachedInputTokens":3662498,"inputTokens":58,"model":"claude-opus-5-5","outputTokens":44436,"runtime":"claude","turns":34,"wallClockMs":543719}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ca0f4e05e864c332","findings":[],"hash":"b5092a7d0042b6f28b4d3694d0d907f3bac620a1a8a9338839e80107f6af8498","nodeId":"1733bd45-5f8e-409d-b815-01e04c5ad12d","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":1,"wallClockMs":22814}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"fa8fc4653a9e883d","findings":[{"citation":"resolved","description":"Q3 (gas). The final-panel fix (8756817) made the three reserve reads copy at most two words of returndata, which closes the returndata bomb. It left the OTHER resource open: the staticcall still forwards gas(), so a listed source that answers well at listing and later spends everything it is given (an upgradeable or externally controlled feed; the same precondition the returndata finding was accepted under) consumes 63/64 of the gas of whatever read reached it. The read itself survives (EIP-150 keeps 1/64 for the caller) and reserveValueUsd() returns the healthy assets' sum, so the NatSpec at lines 206-208 ('it never makes this view revert') holds for the view; but every ACTING path that reads the reserve now needs the whole burn on top of its own work: cash -> _backingPerUnit -> _redemptionReserveBacking -> reserveValue() -> reserveValueUsd(), and earn -> earnLine() -> reserveValue(). Measured on the committed code with one such feed listed: a redemption that cost 299,803 gas before the feed turned reverts out of gas at a 3,000,000 limit and succeeds at 30,000,000 using 29,270,102 gas, i.e. one full block per redemption. Two such listings compound (1/64 of 1/64 is left) and nothing at any limit completes. Reach: needs the governor to have listed a source that misbehaves after listing (48 hours visible), so it is not reachable with the mainnet register (sIMD through the immutable SharePriceFeed) behaving as implemented; it is reachable with the constants as committed the day a second, externally controlled source is listed. Impact is liveness/cost, not loss: redemptions (the peg's defence) and work mints cost a block of gas or fail at ordinary limits until a delisting matures 48 hours later; delisting itself still works (validateReserveAsset returns early for a zero source and never probes). Claims without the property: Treasury.sol:267-272 (_boundedCall) says the fixed-size copy ends the out-of-gas, and 206-208/219-221 promise a dead source 'counts for nothing' rather than pricing the whole call. Smallest fix: forward a fixed gas budget in _boundedCall instead of gas(), e.g. `staticcall(200000, target, ...)` (SharePriceFeed over UsdPriceFeed plus the Chainlink read needs well under 100k; a source that cannot answer inside the budget then counts for nothing, exactly as the NatSpec promises); validateReserveAsset already probes through the same helper, so a source that cannot answer within the budget is refused at proposal for free. Verified: with that one-line change the attached proof passes and the existing reserve tests are unaffected in the paths exercised here.","line":277,"path":"src/Treasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {Parameters} from \"src/Parameters.sol\";\nimport {Treasury} from \"src/Treasury.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract GasFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 days;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external pure returns (bool) {\n        return false;\n    }\n}\n\ncontract GasMirror is ISwarmFeed {\n    ISwarmFeed private immutable p;\n\n    constructor(ISwarmFeed p_) {\n        p = p_;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return p.latestValue();\n    }\n\n    function isStale() external view returns (bool) {\n        return p.isStale();\n    }\n\n    function maxAge() external view returns (uint256) {\n        return p.maxAge();\n    }\n}\n\ncontract GasAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\n/// @dev sIMD shape: 24 decimals over an 18-decimal asset; rate = asset raw per 1e18 share raw.\ncontract GasShare is ERC20 {\n    IERC20 public immutable underlying;\n    uint256 public constant rate = 7.95e12;\n\n    constructor(IERC20 a) ERC20(\"sIMD\", \"sIMD\") {\n        underlying = a;\n    }\n\n    function decimals() public pure override returns (uint8) {\n        return 24;\n    }\n\n    function asset() external view returns (address) {\n        return address(underlying);\n    }\n\n    function convertToAssets(uint256 shares) external pure returns (uint256) {\n        return shares * rate / 1e18;\n    }\n\n    function maxWithdraw(address owner) external view returns (uint256) {\n        return balanceOf(owner) * rate / 1e18;\n    }\n\n    function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) {\n        require(msg.sender == owner, \"owner only\");\n        shares = (assets * 1e18 + rate - 1) / rate;\n        _burn(owner, shares);\n        underlying.transfer(receiver, assets);\n    }\n\n    function deposit(uint256 assets, address receiver) external returns (uint256 shares) {\n        underlying.transferFrom(msg.sender, address(this), assets);\n        shares = assets * 1e18 / rate;\n        _mint(receiver, shares);\n    }\n}\n\ncontract GasToken is ERC20 {\n    constructor() ERC20(\"R\", \"R\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev A well-formed price source at listing time; once armed, every read spends all the gas it is given.\ncontract GasBurnFeed {\n    uint256 public constant maxAge = 1 days;\n    uint256 private immutable price;\n    bool public armed;\n\n    constructor(uint256 p) {\n        price = p;\n    }\n\n    function arm() external {\n        armed = true;\n    }\n\n    function isStale() external view returns (bool) {\n        if (armed) _burn();\n        return false;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        if (armed) _burn();\n        return (price, uint64(block.timestamp));\n    }\n\n    function _burn() private pure {\n        uint256 x;\n        while (true) {\n            x = x + 1;\n        }\n    }\n}\n\n/// @notice Treasury._boundedCall forwards gas() to a listed price source. A source that answers well at\n/// listing and later burns the gas it is given makes every cash (and earn) that reads the reserve cost\n/// ~63/64 of the transaction's gas: at an ordinary 3,000,000 gas limit the redemption reverts out of gas,\n/// where the same redemption cost ~300,000 gas before the feed turned, for the 48 hours a delisting takes.\ncontract Proof_BoundedCallGas is Test {\n    uint256 private constant IMD_ETH = 0.0005 ether; // $1 at ETH 2000\n    address private constant BORROWER = address(0xBA);\n    address private constant REDEEMER = address(0xCA);\n\n    MockIMD private imd;\n    GasShare private share;\n    GasFeed private primary;\n    GasFeed private nhi;\n    ParameterizedVault private vault;\n    Parameters private params;\n    Treasury private treasury;\n    ImdUSD private stable;\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        vm.roll(20_000_000);\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new GasAggregator()).code);\n        imd = new MockIMD();\n        share = new GasShare(imd);\n        primary = new GasFeed(IMD_ETH);\n        nhi = new GasFeed(0.85 ether);\n        vault = new ParameterizedVault(\n            address(share), address(0), address(0), address(primary), address(nhi), address(new GasMirror(primary))\n        );\n        params = vault.parameters();\n        treasury = vault.treasury();\n        stable = vault.stablecoin();\n    }\n\n    function _apply() private {\n        vm.warp(params.pendingEta());\n        primary.set(IMD_ETH);\n        nhi.set(0.85 ether);\n        vm.prank(address(0xA990));\n        params.applyPending();\n    }\n\n    function test_aListedFeedThatBurnsGasDoesNotMakeCashUnaffordable() public {\n        // A listing the register accepts: well-formed answers, a token with decimals.\n        GasToken junk = new GasToken();\n        GasBurnFeed feed = new GasBurnFeed(1 ether);\n        vm.prank(APPROVED_OPERATOR);\n        params.proposeReserveAsset(IERC20(address(junk)), ISwarmFeed(address(feed)), 10_000);\n        _apply();\n        junk.mint(address(treasury), 100 ether);\n        assertEq(treasury.reserveValueUsd(), 100 ether, \"listed and counted while healthy\");\n\n        // A borrower, some supply in a redeemer's hands, a reserve of sIMD, and a day for the lag.\n        vm.prank(APPROVED_OPERATOR);\n        imd.mint(BORROWER, 4000 ether);\n        vm.startPrank(BORROWER);\n        imd.approve(address(vault), 4000 ether);\n        vault.lockIMD(4000 ether);\n        vault.draw(1000 ether);\n        stable.transfer(REDEEMER, 100 ether);\n        vm.stopPrank();\n        vm.prank(APPROVED_OPERATOR);\n        imd.mint(address(this), 100 ether);\n        imd.approve(address(share), 100 ether);\n        share.deposit(100 ether, address(treasury));\n        vm.warp(block.timestamp + 1 days);\n        vm.roll(block.number + 1);\n        primary.set(IMD_ETH);\n        nhi.set(0.85 ether);\n\n        // The feed turns after listing (an upgradeable or externally controlled source).\n        feed.arm();\n\n        // EXPECTED (Treasury NatSpec: a dead source \"counts for nothing\" and \"never makes this view revert\"):\n        // the redemption still costs what it did, about 300,000 gas, so a 3,000,000 limit is ample.\n        // ACTUAL on this code: the staticcall forwards gas(), the feed burns 63/64 of it, and cash reverts.\n        vm.prank(REDEEMER);\n        (bool ok,) = address(vault).call{gas: 3_000_000}(abi.encodeCall(vault.cash, (10 ether, 0, address(0))));\n        assertTrue(ok, \"cash must not run out of gas because a listed feed burns the gas it is forwarded\");\n    }\n}","reproduction":"ParameterizedVault over a 24-decimal share of IMD (rate 7.95e12), IMD $1 (primary 5e14 x ETH/USD 2000), NHI 0.85. Governor proposeReserveAsset(junk, GasBurnFeed answering (1e18, now) and isStale false, 10000), +48h applyPending; junk.mint(treasury, 100e18): reserveValueUsd() == 100e18. Borrower lockIMD(4000e18), draw(1000e18), sends 100e18 imdUSD to a redeemer; treasury holds 100 IMD of shares; +1 day. Control: redeemer cash(10e18, 0, 0) with gas 3,000,000 succeeds (299,803 gas used). Then feed.arm() (every read loops until out of gas). EXPECTED (Treasury NatSpec 206-208, 267-272): the junk counts for nothing and cash costs what it did. ACTUAL: cash{gas: 3_000_000} reverts (out of gas inside the forwarded staticcall, then the 1/64 remainder cannot finish the redemption); cash{gas: 30_000_000} succeeds using 29,270,102 gas; backingPerUnit{gas: 3_000_000}() and earnLine{gas: 3_000_000}() still return. Proof: test/scratch/Proof_BoundedCallGas.t.sol, fails on this code with 'cash must not run out of gas because a listed feed burns the gas it is forwarded'; passes with `staticcall(200000, ...)` in _boundedCall.","severity":"low","snippet":"            success := staticcall(gas(), target, add(data, 32), mload(data), out, 64)","title":"Treasury._boundedCall forwards gas() to a listed price source, so a feed that burns gas makes every cash and earn cost ~63/64 of the transaction (29.3M gas at 30M; revert at 3M) for the 48 hours a del"},{"citation":"resolved","description":"Q4 (rights stranded across a replacement and a wage cycle) and Q1. Rights are priced and credited AT CLAIM (SwarmWorkOracle.claim writes creditedRights and creditedTasks), while _validate's only measure of carried state is vault.totalEarned(), which moves only when earn consumes rights. Between a claim and an earn there is state to carry over: creditedTasks[agentId] (so the successor would re-credit the same cumulative) and creditedRights (which only the old oracle can honour). The documented wage cycle produces exactly that state: governance sets a wage (48h), controllers claim (earn may be refused meanwhile by WorkCeilingReached: empty register and no debt give earnLine 0, or by StaleFeed), governance sets the wage back to 0 (48h; proposeWorkOracle requires it), proposes `new SwarmWorkOracle(vault, maxAge)` (48h) and sets a wage again. Once applied: the old oracle's claim reverts NotTheVaultsOracle (the 8756817 fix), its mintingRights still reports the credit, vault.earn reads the successor and reverts InsufficientRights, and the controller cannot re-claim in the successor until it has accepted a root that lists the agent — a daily receipt lists only agents that worked that day (SwarmWorkOracle.sol:71-75), so an agent who stops working never recovers, and every agent waits for a new attestation bought for the successor. While the wage is 0 nothing can be done to rescue the rights (earn is closed), so the 96 hours of public proposals give the holder no exit; the final-panel fix that closed earn at wage 0 is what makes this window unescapable. Reachable with the constants as committed after one ordinary wage cycle; not on day one (WAGE_WAD = 0, no claims). Bounded: the governor's own 48-hour actions, no theft, the loss is the credited rights of agents absent from the successor's roots (and a delay of at least one attestation for the rest). Claims without the property: Parameters.sol:252-255 ('so it can start from the tallies already credited... before that, there is nothing to carry over'); docs/MAINNET-RUNBOOK.md 7b.3 ('before the first mint, a fresh SwarmWorkOracle built directly for the vault... is proposable'). Smallest fix: treat outstanding credited rights like a mint. Add `uint256 public outstandingRights` to SwarmWorkOracle (+= in claim, -= in consumeRights) and in _validate probe the current oracle with a raw staticcall for outstandingRights(); if it answers a nonzero word, require the successor's predecessor() as for `minted` (a MockWorkOracle, which does not answer, is unaffected). Verified: with that change (3 lines in SwarmWorkOracle, 2 in Parameters) the attached proof passes. If the requester prefers to keep the replacement open, the NatSpec and runbook sentence must instead say that claims made under an earlier wage are stranded by a replacement and must be re-claimed against a root the successor accepts.","line":400,"path":"src/Parameters.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {Parameters} from \"src/Parameters.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {SwarmWorkOracle} from \"src/SwarmWorkOracle.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {\n    APPROVED_OPERATOR,\n    CHAINLINK_ETH_USD,\n    TREASURY_FACTORY,\n    WORK_ORACLE_FACTORY,\n    WORK_ORACLE_SENTINEL,\n    ERC8004_ADAPTER\n} from \"src/DeploymentConfig.sol\";\n\ncontract SrFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 days;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external pure returns (bool) {\n        return false;\n    }\n}\n\ncontract SrMirror is ISwarmFeed {\n    ISwarmFeed private immutable p;\n\n    constructor(ISwarmFeed p_) {\n        p = p_;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return p.latestValue();\n    }\n\n    function isStale() external view returns (bool) {\n        return p.isStale();\n    }\n\n    function maxAge() external view returns (uint256) {\n        return p.maxAge();\n    }\n}\n\ncontract SrAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract SrShare is ERC20 {\n    IERC20 public immutable underlying;\n    uint256 public constant rate = 7.95e12;\n\n    constructor(IERC20 a) ERC20(\"sIMD\", \"sIMD\") {\n        underlying = a;\n    }\n\n    function decimals() public pure override returns (uint8) {\n        return 24;\n    }\n\n    function asset() external view returns (address) {\n        return address(underlying);\n    }\n\n    function convertToAssets(uint256 shares) external pure returns (uint256) {\n        return shares * rate / 1e18;\n    }\n\n    function maxWithdraw(address owner) external view returns (uint256) {\n        return balanceOf(owner) * rate / 1e18;\n    }\n\n    function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) {\n        require(msg.sender == owner, \"owner only\");\n        shares = (assets * 1e18 + rate - 1) / rate;\n        _burn(owner, shares);\n        underlying.transfer(receiver, assets);\n    }\n\n    function deposit(uint256 assets, address receiver) external returns (uint256 shares) {\n        underlying.transferFrom(msg.sender, address(this), assets);\n        shares = assets * 1e18 / rate;\n        _mint(receiver, shares);\n    }\n}\n\n/// @dev The production work oracle with a test-only seeding door (the attester's key is not ours).\ncontract SrSeedableWork is SwarmWorkOracle {\n    constructor(address vault_, uint256 maxAge_) SwarmWorkOracle(vault_, maxAge_) {}\n\n    function seed(uint256 v) external {\n        _accept(v, uint64(block.timestamp));\n    }\n}\n\ncontract SrSeedableWorkFactory {\n    function create(uint256 maxAge_) external returns (SrSeedableWork o) {\n        o = new SrSeedableWork(msg.sender, maxAge_);\n    }\n}\n\n/// @notice Parameters.proposeWorkOracle keys \"there is nothing to carry over\" on vault.totalEarned() alone.\n/// Rights are credited at claim, so an oracle can hold credited, unconsumed rights with nothing minted; a\n/// fresh SwarmWorkOracle is then accepted as the successor and those rights are stranded: the old oracle\n/// refuses further claims (NotTheVaultsOracle), the vault reads the new one (InsufficientRights).\ncontract Proof_StrandedRights is Test {\n    uint256 private constant IMD_ETH = 0.0005 ether;\n    uint256 private constant AGENT = 51450;\n    address private constant CONTROLLER = address(0xA11CE);\n\n    MockIMD private imd;\n    SrFeed private primary;\n    SrFeed private nhi;\n    ParameterizedVault private vault;\n    Parameters private params;\n    SrSeedableWork private work;\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        vm.roll(20_000_000);\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(WORK_ORACLE_FACTORY, address(new SrSeedableWorkFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new SrAggregator()).code);\n        imd = new MockIMD();\n        SrShare share = new SrShare(imd);\n        primary = new SrFeed(IMD_ETH);\n        nhi = new SrFeed(0.85 ether);\n        vault = new ParameterizedVault(\n            address(share), address(0), WORK_ORACLE_SENTINEL, address(primary), address(nhi), address(new SrMirror(primary))\n        );\n        params = vault.parameters();\n        work = SrSeedableWork(address(vault.oracle()));\n    }\n\n    function _apply() private {\n        vm.warp(params.pendingEta());\n        primary.set(IMD_ETH);\n        nhi.set(0.85 ether);\n        vm.prank(address(0xA990));\n        params.applyPending();\n    }\n\n    function _setWage(uint256 wad) private {\n        vm.prank(APPROVED_OPERATOR);\n        params.proposeWage(wad);\n        _apply();\n    }\n\n    /// @dev A two-leaf StandardMerkleTree holding the agent's leaf and one other.\n    function _tree(uint256 id, uint32 accepted, uint64 cumulative) private pure returns (bytes32 root, bytes32[] memory proof) {\n        bytes32 a = keccak256(bytes.concat(keccak256(abi.encode(id, accepted, cumulative))));\n        bytes32 b = keccak256(bytes.concat(keccak256(abi.encode(uint256(1), uint32(1), uint64(1)))));\n        root = a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));\n        proof = new bytes32[](1);\n        proof[0] = b;\n    }\n\n    function test_aReplacementIsRefusedWhileTheCurrentOracleHoldsCreditedUnconsumedRights() public {\n        // Before any claim a fresh successor is a valid proposal (nothing to carry over), and is cancelled.\n        SwarmWorkOracle early = new SwarmWorkOracle(address(vault), 1 days);\n        vm.prank(APPROVED_OPERATOR);\n        params.proposeWorkOracle(address(early));\n        vm.prank(APPROVED_OPERATOR);\n        params.cancel();\n\n        // Governance turns the wage on; an agent's controller claims 500 attested tasks and mints nothing.\n        _setWage(1 ether);\n        (bytes32 root, bytes32[] memory proof) = _tree(AGENT, 500, 500);\n        work.seed(uint256(root));\n        work.recordRoot();\n        vm.mockCall(\n            ERC8004_ADAPTER, abi.encodeWithSignature(\"isController(uint256,address)\", AGENT, CONTROLLER), abi.encode(true)\n        );\n        vm.prank(CONTROLLER);\n        assertEq(work.claim(AGENT, 500, 500, proof, root), 500 ether);\n        assertEq(work.mintingRights(CONTROLLER), 500 ether, \"credited, unconsumed\");\n        assertEq(vault.totalEarned(), 0, \"nothing minted\");\n\n        // Governance turns the wage off, as proposeWorkOracle requires, and proposes a fresh SwarmWorkOracle.\n        _setWage(0);\n        SwarmWorkOracle successor = new SwarmWorkOracle(address(vault), 1 days);\n        // EXPECTED: refused, because the current oracle still carries credited rights the successor cannot\n        // carry over (the same rule that applies once anything has been minted).\n        // ACTUAL on this code: accepted; applied 48 hours later the 500e18 rights are stranded in the old\n        // oracle (it refuses claims once superseded; the vault reads the successor, which holds nothing).\n        vm.prank(APPROVED_OPERATOR);\n        vm.expectRevert();\n        params.proposeWorkOracle(address(successor));\n    }\n}","reproduction":"ParameterizedVault built with WORK_ORACLE_SENTINEL (factory etched to a SwarmWorkOracle subclass whose only addition is a seeding door for the root; claim/rights/consume/wage are production code), 24-decimal share collateral, IMD $1, NHI 0.85. Governor proposeWage(1e18), +48h apply. Root over leaf (51450, 500, 500) seeded and recordRoot(); ERC8004_ADAPTER mocked to confirm CONTROLLER; CONTROLLER claim(...) returns 500e18; old.mintingRights(CONTROLLER) == 500e18; vault.totalEarned() == 0. Governor proposeWage(0), +48h apply; B = new SwarmWorkOracle(vault, 1 days); proposeWorkOracle(B). EXPECTED per Parameters.sol:254-255: nothing is carried over because nothing exists to carry, or the proposal is refused as it is after a first mint. ACTUAL: the proposal is accepted and applied 48h later; vault.oracle() == B; after proposeWage(1e18) +48h: old.mintingRights(CONTROLLER) == 500e18, B.mintingRights(CONTROLLER) == 0, vault.earn(1e18) from CONTROLLER reverts InsufficientRights (earnLine 2500e18 with 10,000e18 of warmed debt), old.claim(...) reverts NotTheVaultsOracle. Proof: test/scratch/Proof_StrandedRights.t.sol, fails on this code with 'next call did not revert as expected'; passes with the outstandingRights probe described above (measured: PASS).","severity":"low","snippet":"            bool minted = vault.totalEarned() != 0;","title":"proposeWorkOracle keys 'nothing to carry over' on totalEarned alone, so a fresh SwarmWorkOracle is accepted as successor while the current oracle holds credited, unminted rights, which the replacement"},{"citation":"resolved","description":"Q5 (is the statement complete and are the bounds as described). The bounds are as described: every proposal waits TIMELOCK = 48 hours (Governed.sol:25, 58, 83), one slot at a time, and _validate runs again at application; reserveValueOf caps each asset at MAX_RESERVE_VALUE (Treasury.sol:238) and saturates the sum; earn is refused while parameters.wage() == 0 (ParameterizedVault._earnOpen, CDPVault.earn line 491), so a replacement oracle alone mints nothing; a hostile oracle cannot be installed while the wage is nonzero and cannot replace a SwarmWorkOracle after a first mint without a predecessor(). Two things the statement does not say. (1) Hours: the sentence counts 96 for listing + oracle, then adds that a wage proposal must also be public 48 hours; with one slot the three steps it describes are sequential, 144 hours. Minting through a hostile oracle against the ratio term alone (25% of backedDebt, up to $250k at the $1M LINE with no listing) is the 96-hour path. (2) The listing step has a consequence of its own that needs neither oracle nor wage and is the opposite direction from 'lowering the redemption backing': reserveValueUsd is the first term of ParameterizedVault._redemptionReserveBacking (ParameterizedVault.sol:178-182), which _backingPerUnit reads for every cash. A junk listing therefore raises the figure a redeemer is PAID against, and any unprivileged redeemer then takes real sIMD (the Treasury's reserve first, then candidates' collateral) at par while the true backing is below par, which is the unprivileged amplifier the validation gates ask for; the remaining holders are left with the junk. Measured: with backing at 0.42 (price fall), an honest 10 imdUSD redemption pays 2.615e24 raw sIMD; after one 48-hour listing of a token priced 1e30 the same redemption pays 6.226e24, 2.38x, with backingPerUnit() reading 1e18. The same listing also lifts earnLine, which the statement does cover. This is the governor's intended, visible power and is reported as the statement's completeness, not as a bypass; the earlier panel classed the whole power as a trust assumption (info) and that classification is kept. Smallest fix: restate lines 54-62 as 'a reserve listing sets the reserve term of earnLine AND of the redemption backing (backingPerUnit, cash), so a listing alone, after 48 hours, lets redeemers be paid at par against a reserve the governor priced; with a replacement oracle (48 h) and a wage (48 h), 144 hours in all, it is also minting with no collateral and no attested work; against the ratio term alone the oracle and wage steps, 96 hours, mint up to earnMat of the existing backed debt.'","line":58,"path":"src/Parameters.sol","reproduction":"Read-only against the committed code plus one measurement (test/scratch/Probe2.t.sol test_probeJunkListingLiftsRedemptionPayout, passes as a demonstration). Hours: Governed.TIMELOCK = 48 hours; Governed._propose reverts ProposalPending while pendingEta != 0, so listing, oracle and wage are three sequential 48-hour windows = 144 hours; the statement says 96. Payout: ParameterizedVault over a 24-decimal share, IMD $1; borrower lockIMD(4000e18), draw(1000e18), 100e18 imdUSD to a redeemer, Treasury holds 100 IMD of sIMD; +1 day; IMD falls to $0.20 (+1 day for the lag): backingPerUnit() == 0.42e18; cash(10e18, 0, 0) pays 2615094339622641503144654 raw sIMD. Same state, governor proposeReserveAsset(junk ERC-20, feed answering 1e30, 10000), +48h apply, 1e18 junk at the Treasury: reserveValueUsd() == 1e30, backingPerUnit() == 1e18, cash(10e18, 0, 0) pays 6226415094339622641509433 raw sIMD. EXPECTED per the statement ('lowering the redemption backing', minting only 'together' with the oracle and wage): a listing alone changes only earnLine. ACTUAL: a listing alone more than doubles what a redeemer is paid in real collateral.","severity":"info","snippet":"/// Together, after 96 hours of public proposals, that is minting with no collateral and no attested","title":"The governor's minting trust assumption in Parameters is incomplete: the hour count is 144 for the three-step path it describes (96 for oracle + wage against existing debt), and a reserve listing alon"},{"citation":"resolved","description":"READ IN FULL: src/Parameters.sol, src/Governed.sol, src/Treasury.sol, src/TreasuryFactory.sol, src/WorkOracleFactory.sol, src/SwarmWorkOracle.sol, src/ParameterizedVault.sol, src/CDPVault.sol (earn, cash, cover, _backingPerUnit, _securedCollateralValue, the lag, oracle/_validateOracle, drip/chi), src/SwarmFeed.sol, src/OracleAsker.sol, src/UsdPriceFeed.sol, src/SharePriceFeed.sol, src/ImdUSD.sol, src/MockWorkOracle.sol, src/DeploymentConfig.sol, the six interfaces, the fix diff of 8756817 for these files, docs/AUDIT-FINAL-PANEL-GOVERNANCE-2026-10-07.md and the findings tables of the vault, oracle and FINAL/FINAL-2 rounds, docs/MAINNET-RUNBOOK.md sections 7, 7b and 8, docs/AUDIT-FIX-PLAN-2026-10-05.md (accepted residuals). COULD NOT REACH: StakedIMD (sIMD) itself, the live Chainlink aggregator, the ERC-8004 adapter and the Intake; their behaviour rests on the repository's mocks and fork notes (the sIMD same-block hold griefing of fundOracle is the accepted residual D5 and is not re-reported). No static analyser was run; the probes and proofs under test/scratch/ were run with forge 1.8.3. Q1 TIMELOCK AND BOUNDS: no change lands sooner than 48 hours, outside its bounds or by another route. _propose is onlyGovernor (APPROVED_OPERATOR, a source constant), one slot, _validate at proposal and again at application, every bound a constant (duty <= 1000, skew in [100, 2000], chip + cut <= 10000, line != 0, earnMat <= 2500, wage <= 1e18, budget <= 100e18, divisor in [1, 8], stream <= 500e18 with a payee, gap in [25, 100]); Treasury.setReserveAsset accepts only registrar() == vault.parameters(). The Economics apply checkpoints the fee index (vault.drip()) before the new duty is readable. Blocking: a WorkOracle proposal can no longer be blocked by a third party (earn is refused at wage 0, so totalEarned cannot move during the window; a SwarmWorkOracle successor's vault() and mintingRights are immutable/pure); a ReserveAsset listing re-probes the token and source at application, which only their controllers can change; the shipped feeds (UsdPriceFeed, SharePriceFeed) always answer well-formed words. Chosen-moment application after eta is the accepted audit note at Governed.sol:73-79. Q2 TREASURY EXITS: withdraw (operator; refuses gem() and listed assets; stablecoin floored at vault.totalBadDebt), withdrawNative (operator, any amount, ETH cannot be listed), payStream (anyone; <= streamPerDay <= 500/UTC day, floored at bad debt, a day not claimed is not carried over), fundOracle (anyone; <= oracleBudget <= 100/UTC day; top-up to one day's budget measured on the asker's IMD balance; plain IMD first unless IMD is listed, then sIMD unwrapped via maxWithdraw; refuses a codeless asker), redeemIMD (vault only, sized by cash), cover (anyone, <= the drained position's accrued debt, syncs the Treasury's imdUSD before and after the burn). Day boundaries use block.timestamp / 1 days with the counter reset on a new day; a budget lowered below today's spend pays nothing more, raised it pays the difference; two days' budgets across midnight is the per-UTC-day design. Rounding: withdraw(assets) burns shares rounded up against the Treasury, the asker receives exactly `assets`; maxWithdraw bounds the share leg. Accounting across the plain-IMD path: _withdraw credits the unsynced IMD arrival to totalReceived[IMD] before moving lastSynced[IMD] to the post-transfer balance and re-derives it after; the share leg does the same on lastSynced[sIMD] through _withdrawUnderlying; a later sync of either token credits nothing twice (test/OracleBudget.t.sol covers the share leg; the plain leg traced here). Q3 RESERVE VALUATION: _boundedCall's memory use is sound (it writes 64 bytes at the free memory pointer without advancing it, which Solidity's memory-safe rules allow as temporary memory; first is read only when returndatasize >= 32, second only when >= 64, else forced to max and refused as a uint64); a token that reverts on balanceOf or returns short counts for nothing","line":63,"path":"src/Parameters.sol","reproduction":"Not applicable: a coverage record. Every statement above was checked by reading the cited lines at 8756817; the measured figures are in test/scratch/Probe.t.sol and Probe2.t.sol (run with forge test --match-path 'test/scratch/Probe*').","severity":"info","snippet":"contract Parameters is Governed {","title":"Not a defect: coverage record and answers to the six questions for the governance and Treasury sweep at 8756817"}],"hash":"c9670daa0a2b5ec4c6a5a5119a67dbf9eacd0108a1385cb47ad3f5e6ca8cee86","nodeId":"0161df7a-75c9-445d-90c0-16d9aea627bb","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds four entries: two lows with proofs that fail on the committed code and pass with the stated fixes, one info on the trust-assumption statement, and one info carrying the coverage record and answers to the six questions. No tracked file was changed: the temporary patches used to confirm the two fixes were reverted with `git checkout`, and `git status` is clean.\n\n**Findings**\n\n- **Low, `src/Treasury.sol:277`.** `_boundedCall` forwards all gas to a listed price source. The final-panel fix bounded the returndata copy but not the gas, so a source that answers well at listing and later burns what it is given makes every `cash` and `earn` cost 63/64 of the transaction for the 48 hours a delisting takes. Measured: a redemption that cost 299,803 gas reverts at a 3M limit and uses 29.27M gas at 30M. Fix is one word: a fixed gas budget in the staticcall. Proof passes with it.\n- **Low, `src/Parameters.sol:400`.** A pre-mint oracle replacement is accepted while the current oracle holds credited, unminted rights. Rights are priced at claim, so after a wage on, claim, wage off, replace, wage on cycle the 500e18 rights are stranded: the old oracle refuses claims once superseded and the vault reads the new one. The NatSpec says \"there is nothing to carry over\". Fix: track outstanding rights in `SwarmWorkOracle` and have `_validate` treat them like a mint. Proof passes with that five-line change.\n- **Info, `src/Parameters.sol:58`.** The governor minting statement counts 96 hours where its three sequential steps take 144, and omits that a reserve listing alone feeds `backingPerUnit`. Measured: with true backing at 0.42, one junk listing lifts it to par and a 10 imdUSD redemption pays 2.38x the honest sIMD. Any redeemer is the amplifier.\n- **Info, `src/Parameters.sol:63`.** Coverage record and the Q1 to Q6 answers, including what I could not reach (live sIMD, Chainlink, the ERC-8004 adapter, the Intake).\n\n**What held up under probing.** The newest fixes behave as described: earn is closed at wage 0, so a pending oracle replacement can no longer be blocked by a third party and no rights are consumable in two oracles at once. The two-word copy in `_boundedCall` is memory-safe and its length handling is correct. `fundOracle` spends plain IMD first, and its accounting credits arrivals before moving the baseline on both token legs. The sIMD dust-transfer hold is the accepted residual D5 and was not re-reported.","treeHash":null,"usage":{"cachedInputTokens":5557828,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":80860,"runtime":"claude","turns":46,"wallClockMs":1149868}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"559cfaaab2c0d013","findings":[{"citation":"resolved","description":"Q3 (gas), a gap left by the 8756817 fix for the final governance panel's low (returndata bomb). _boundedCall copies at most 64 bytes, so the copy can no longer run the caller out of gas, but it passes `gas()` to the staticcall, so the callee receives 63/64 of everything the Treasury has. A listed price source (or token) that burns what it is forwarded therefore leaves reserveValueUsd with 1/64 of its gas after each read. One such listing does not revert the sum (the loop finishes on the remainder) but every transaction that reads it burns essentially all the gas it was given: the scratch test measures 4,927,677 of 5,000,000 gas consumed by one reserveValueUsd call whose asset counts for nothing, and earn, cash, backingPerUnit and the frontend's earnLine all pay the same. Wallet estimation for a cash or earn then lands near the block limit, and at ~13M+ gas per redemption the peg-defence channel is priced out. With three listings against gas-burning sources, (1/64)^3 of 30M is about 114 gas and reserveValueUsd itself reverts out of gas, which takes earnLine, earn, reserveValue, _redemptionReserveBacking, backingPerUnit and every cash down with it until a 48-hour delisting matures. That contradicts reserveValueUsd's NatSpec (lines 206-208: 'it never makes this view revert') and reserveValueOf's (219-221), the promise the bounded reads were written to keep. Precondition, as for the accepted returndata-bomb low: the governor has listed an asset against a mutable/upgradeable source that later misbehaves; the mainnet plan (sIMD through the immutable SharePriceFeed) is not exposed, and listing is governor-only behind the timelock, so this is low. Smallest fix: forward a bounded gas budget per read, e.g. `staticcall(200000, target, ...)` in _boundedCall (a figure comfortably above SharePriceFeed's two nested reads; verified: both scratch tests pass and test/ReserveValuation.t.sol stays 20/20 with that one-word change), and reword the NatSpec if the design prefers to leave gas unbounded.","line":277,"path":"src/Treasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {Parameters} from \"src/Parameters.sol\";\nimport {Treasury} from \"src/Treasury.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\n/// @dev A listed price source that answers well-formed words at listing and later burns every unit of gas\n/// it is forwarded (an upgradeable or hostile feed after a 48-hour listing).\ncontract GasBurningFeed is ISwarmFeed {\n    bool public burn;\n    uint256 public constant maxAge = 1 days;\n\n    function setBurn(bool on) external {\n        burn = on;\n    }\n\n    function isStale() external view returns (bool) {\n        if (burn) {\n            assembly {\n                invalid()\n            }\n        }\n        return false;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (1e18, uint64(block.timestamp));\n    }\n}\n\ncontract OffsetFeed is ISwarmFeed {\n    uint256 private immutable v;\n    uint256 public constant maxAge = 1 days;\n\n    constructor(uint256 v_) {\n        v = v_;\n    }\n\n    function isStale() external pure returns (bool) {\n        return false;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (v, uint64(block.timestamp));\n    }\n}\n\ncontract Mirror is ISwarmFeed {\n    ISwarmFeed private immutable src;\n\n    constructor(ISwarmFeed s) {\n        src = s;\n    }\n\n    function isStale() external view returns (bool) {\n        return src.isStale();\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return src.latestValue();\n    }\n\n    function maxAge() external view returns (uint256) {\n        return src.maxAge();\n    }\n}\n\ncontract Agg {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract Rsv is ERC20 {\n    constructor() ERC20(\"R\", \"R\") {}\n\n    function mint(address to, uint256 a) external {\n        _mint(to, a);\n    }\n}\n\n/// @notice Treasury._boundedCall bounds the returndata copy but forwards all gas (`gas()`), so a listed\n/// source that burns what it is forwarded makes every reader of reserveValueUsd pay the whole\n/// transaction's gas, and with three such listings the sum reverts at the block gas limit.\ncontract ReserveReadGasTest is Test {\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    Treasury private treasury;\n    Parameters private params;\n    GasBurningFeed private feed;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new Agg()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        OffsetFeed primary = new OffsetFeed(0.0005 ether); // $1 per IMD at $2000/ETH\n        OffsetFeed nhi = new OffsetFeed(0.85 ether);\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(nhi), address(new Mirror(primary))\n        );\n        treasury = vault.treasury();\n        params = vault.parameters();\n        feed = new GasBurningFeed();\n    }\n\n    /// @dev Lists `n` distinct tokens, each holding 100 units at the Treasury, against the same feed, each\n    /// through the real 48-hour proposal.\n    function _list(uint256 n) private {\n        for (uint256 i; i < n; ++i) {\n            Rsv rsv = new Rsv();\n            rsv.mint(address(treasury), 100e18);\n            vm.prank(APPROVED_OPERATOR);\n            params.proposeReserveAsset(IERC20(address(rsv)), feed, 10_000);\n            vm.warp(params.pendingEta());\n            params.applyPending();\n        }\n        assertEq(treasury.reserveValueUsd(), n * 100e18, \"listed and valued while the feed behaves\");\n    }\n\n    function test_oneBurningFeedMakesEveryReadCostTheWholeTransaction() public {\n        _list(1);\n        feed.setBurn(true);\n        uint256 before = gasleft();\n        (bool ok,) = address(treasury).staticcall{gas: 5_000_000}(abi.encodeCall(treasury.reserveValueUsd, ()));\n        uint256 used = before - gasleft();\n        assertTrue(ok, \"one burning source: the sum survives on the 1/64 it keeps\");\n        // EXPECTED: a source that counts for nothing costs about one external call. ACTUAL: the whole 5M\n        // forwarded is burned; the same happens inside every earn, cash and backingPerUnit.\n        assertLt(used, 1_000_000, \"a read must not cost the whole transaction's gas\");\n    }\n\n    function test_threeBurningListingsRevertTheSumAtTheBlockGasLimit() public {\n        _list(3);\n        feed.setBurn(true);\n        // EXPECTED (Treasury.reserveValueUsd NatSpec, lines 206-208: 'it never makes this view revert'):\n        // the three assets count for nothing and the view completes. ACTUAL: each read keeps 1/64 of what\n        // it had, (1/64)^3 of 30M is about 114 gas, and the sum runs out of gas.\n        (bool ok,) = address(treasury).staticcall{gas: 30_000_000}(abi.encodeCall(treasury.reserveValueUsd, ()));\n        assertTrue(ok, \"reserveValueUsd must not revert because of listed sources\");\n    }\n}","reproduction":"test/scratch/ReserveReadGas.t.sol (both tests FAIL on this code, PASS with `staticcall(200000, ...)`). ParameterizedVault over MockIMD at $1 (primary 0.0005 ETH, Chainlink 2000e8 etched), NHI 0.85. GasBurningFeed answers isStale()=false / latestValue()=(1e18, now) at listing; APPROVED_OPERATOR proposeReserveAsset(token holding 100e18 at the Treasury, feed, 10000), +48h applyPending: reserveValueUsd()==100e18. feed.setBurn(true): isStale() now executes INVALID, consuming all forwarded gas. (1) treasury.reserveValueUsd{gas: 5_000_000}(): EXPECTED a read of a source that counts for nothing costs about one external call (< 1,000,000 gas); ACTUAL succeeds but consumes 4,927,677 gas. (2) Three tokens listed against the same feed, feed.setBurn(true), treasury.reserveValueUsd{gas: 30_000_000}(): EXPECTED (NatSpec 206-208) returns 0 for the three and completes; ACTUAL the staticcall returns ok == false (out of gas).","severity":"low","snippet":"            success := staticcall(gas(), target, add(data, 32), mload(data), out, 64)","title":"Treasury._boundedCall bounds the returndata copy but forwards all gas, so a listed source that burns its gas makes every reserveValueUsd reader (earnLine, earn, backingPerUnit, cash) pay the whole tra"},{"citation":"resolved","description":"Q5. The statement added in 8756817 (lines 54-62) is right about the mechanism and the bounds (48 hours per step, MAX_RESERVE_VALUE per asset, earn refused at wage 0) but is incomplete in two places. (a) Timing: 'Together, after 96 hours of public proposals, that is minting with no collateral and no attested work' counts the listing and the oracle only; the same paragraph then says the oracle step mints nothing until a wage proposal has also been public for 48 hours, so the path from the launch state (wage 0) is three serial proposals, 144 hours, and from a running wage it is four (wage to 0 first), 192 hours. The two sentences give different totals for the same path. (b) Scope of the listing power: a listing against any shape-valid feed sets not only the reserve term of earnLine but also the `others` term of ParameterizedVault._redemptionReserveBacking, which feeds CDPVault._backingPerUnit. A single listing valued at MAX_RESERVE_VALUE therefore reads backingPerUnit at par whatever the collateral is worth, and cash pays (1 - fee) of par out of the Treasury's real sIMD and eligible candidates' collateral while true backing is below par; the paragraph mentions 'lowering the redemption backing' only as a consequence of dilution, not that the listing itself raises the figure redeemers are paid against. No oracle, wage or 'minting' is needed for this half, so it is a 48-hour power, not a 144-hour one. Trust assumption on the APPROVED_OPERATOR key, reported as the task asks (is the statement complete), not as a bypass. Smallest fix: state the total as three proposals (144 hours) and add one sentence that a listing also sets the reserve term of the redemption backing (backingPerUnit), so a bogus listing makes redemptions pay par.","line":58,"path":"src/Parameters.sol","reproduction":"test/scratch/ListingLiftsPayout.t.sol (PASSES: demonstrates the state). WorkBackingFixture (OpenWorkVault over MockIMD at $1, empty register). BORROWER locks 2,000 IMD and draws 1,000 imdUSD; one day warms the lag; the price falls to $0.40. backingPerUnit() == 800000000000000000 (0.8: $800 of collateral behind 1,000 imdUSD). APPROVED_OPERATOR proposeReserveAsset(ReserveTestToken holding 1e18 at the Treasury, TestSwarmFeed answering 1e30, 10000), +48h applyPending. EXPECTED per the statement: the listing only sets 'the reserve term of earnLine'. ACTUAL: reserveValueUsd() == 1e30 and backingPerUnit() == 1e18 (par), so cash now pays (1 - fee) of par per imdUSD out of the vault's real collateral and reserve.","severity":"info","snippet":"/// Together, after 96 hours of public proposals, that is minting with no collateral and no attested","title":"Parameters' trust statement understates the governor's minting path (144 hours of proposals from launch, not 96) and omits that a reserve listing alone lifts backingPerUnit, and so every redemption pa"}],"hash":"d67413798925f4abc5734a9be649e5f4cb622dc1cfcbe6c30cc836bc51857565","nodeId":"f9b250a9-587c-46b0-bbff-4480f941fbf2","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds two findings, both verified against the tree, and no tracked file was changed. The only writes are the findings file and two scratch tests under `test/scratch/`.\n\n## Findings\n\n**1. Low, `src/Treasury.sol:277`, `_boundedCall`.** The 8756817 fix bounds the returndata copy but still forwards `gas()`, so a listed source that burns what it receives leaves each read with 1/64 of its gas. One such listing does not revert the sum, but every `earn`, `cash`, `backingPerUnit` and `earnLine` that reads it burns essentially all the gas it is given. The scratch test measures 4,927,677 of 5,000,000 gas consumed by one call whose asset counts for nothing. With three such listings the sum itself reverts at a 30M limit, contradicting the \"never makes this view revert\" NatSpec at lines 206 to 208. Precondition is the same as the accepted returndata-bomb low: a governor-listed source that later misbehaves. Smallest fix is a per-read gas budget, for example `staticcall(200000, ...)`. I verified that one-word change makes both scratch tests pass and leaves `test/ReserveValuation.t.sol` green, then restored the file. Proof attached: `test/scratch/ReserveReadGas.t.sol`, which fails on this code.\n\n**2. Info, `src/Parameters.sol:58`, trust statement.** The path from the launch state is three serial proposals, 144 hours, not the 96 stated. The statement also omits that a listing alone sets the `others` term of the redemption backing, so one listing valued at the per-asset cap reads `backingPerUnit` at par whatever collateral is worth. The demonstration test moves it from 0.8 to 1.0 with no oracle or wage step.\n\n## Answers to the numbered questions\n\n1. **Timelock and bounds.** No change applies early, outside its bounds, or by anyone but the governor. Every `propose*` routes through `onlyGovernor`, bounds are constants, and validation runs at both ends. The earlier griefing of a pending oracle replacement is closed because `earn` is refused at wage 0 and the wage cannot change while a proposal occupies the slot. The matured-but-unapplied note stands as documented.\n2. **Treasury exits.** All six are bounded as documented. The plain-IMD path credits arrivals before moving the baseline and clamps after. Day counters, rate changes mid-day, and the top-up logic behave correctly. The accepted D5 dust-grief does not reach the plain-IMD path: I fetched the verified StakedIMD source, and its `maxWithdraw` returns 0 under the hold, so `fromShares` is 0 and the plain transfer proceeds.\n3. **Reserve valuation.** The assembly is memory-safe: it uses memory past the free pointer as temporary scratch and reads both words inside the block. Short, malformed, oversized and reverting answers all count for zero. The one remaining gap is gas, finding 1.\n4. **Work oracle.** Rights cannot be consumed in two oracles. A superseded oracle refuses claims, and the probe returns true for a vault without `oracle()` or an EOA. Rights claimed under a wage survive a wage cycle and are re-claimable in a successor built directly, which is the documented position. Directly constructed successors validate; factory-made ones are refused as documented.\n5. **Governor minting power.** Mechanism and bounds are as described. The statement is incomplete on timing and on the listing's effect on redemption payout, finding 2.\n6. **Day one.** No dead state without a revival in section 7. The asker seed, the keeper fallback, operator `withdraw(IMD)` to the asker, and the plain-IMD path in `fundOracle` cover every state I could construct.\n\n## NatSpec claims without the property\n\nOnly one: `Treasury.sol` lines 206 to 208 and 219 to 221, covered by finding 1. All other comments corrected in 8756817 now match the code.\n\n## Coverage\n\nRead in full: Parameters, Governed, Treasury, TreasuryFactory, WorkOracleFactory, SwarmWorkOracle, ParameterizedVault, CDPVault, DeploymentConfig, SwarmFeed, UsdPriceFeed, SharePriceFeed, ImdUSD, OracleAsker, all interfaces, th","treeHash":null,"usage":{"cachedInputTokens":3753501,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":58590,"runtime":"claude","turns":45,"wallClockMs":946383}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b3280f20e86b4e0d","findings":[],"hash":"dd27a6c4f9471d0f08094bb1dda29813381a6bdee5d76dfec792a4cd388c12d8","nodeId":"f9b250a9-587c-46b0-bbff-4480f941fbf2","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":4,"wallClockMs":322065}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b5ff2623f878600d","findings":[],"hash":"e5da12e3b23b95736347ba1d6bae3d5708be994ef260d5cadb8129e36e3ec9fe","nodeId":"d5683b3c-0b1a-4d84-acb9-199548223c82","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":4,"wallClockMs":446430}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"feba2a869621cab2","findings":[{"citation":"resolved","description":"Q3 (gas), a gap left by the 8756817 fix for the final governance panel's low #3. The fix copies at most two words of returndata, which closes the returndata bomb, but the staticcall still passes gas(), so a listed price source or token whose isStale()/latestValue()/balanceOf() spends everything it is given (an upgradeable or externally controlled feed that answered well at listing and turns later; validateReserveAsset probes shape only, and a probe that burned gas at proposal would be refused there) keeps 63/64 of whatever the Treasury has at each read (EIP-150). Measured on the committed code: with ONE such feed listed, a redemption that cost 299,803 gas before the feed turned reverts out of gas at a 3,000,000 limit and succeeds only at 30,000,000 (29,270,102 used); treasury.reserveValueUsd{gas: 5M}() returns but consumes 4,927,677 gas. With THREE such listings (1/64 of 1/64 of 1/64 of 30M is about 114 gas) reserveValueUsd{gas: 30M}() itself reverts, and with it earnLine, earn, reserveValue, _redemptionReserveBacking, backingPerUnit and every cash, until three sequential 48-hour delistings mature (one slot: 144 hours). Call sequence: any caller -> ParameterizedVault.cash / earn / backingPerUnit -> _redemptionReserveBacking or earnLine -> reserveValue -> Treasury.reserveValueUsd -> reserveValueOf -> _reservePrice -> _readBool -> _boundedCall. Reachable with the constants as committed only after the governor has listed a source that misbehaves after listing (48 hours visible); the planned mainnet register (sIMD through the vault's own immutable SharePriceFeed) cannot do it. Impact is liveness and cost, not loss: a dead source still counts for zero, so nothing is mispriced, but the peg's redemption channel and the work ceiling are priced out or shut for two days per delisting. Delisting itself still works (validateReserveAsset returns early for a zero source). NatSpec claiming a property the code does not have: Treasury.sol:206-208 ('it never makes this view revert'), 219-221 and 267-272 (the fixed-size copy presented as ending the out-of-gas). Smallest fix: forward a fixed stipend instead of gas() in _boundedCall, e.g. `staticcall(200000, target, add(data, 32), mload(data), out, 64)` (SharePriceFeed over UsdPriceFeed plus the Chainlink read needs well under 100k), so a source that cannot answer inside the budget counts for nothing exactly as the NatSpec promises; optionally require gasleft() > 200000 * 64 / 63 before the call so a caller cannot starve an honest read on purpose (a starved read only lowers the caller's own payout or refuses their earn). Verified: with that one-word change all three attached specialist proofs pass and test/ReserveValuation, Treasury, TreasuryGuards, OracleBudget, WorkCeiling and Redemption stay green (122/122). Merged from audit_permissions, audit_economics, audit_flow and audit_math (one defect, four reproductions, all confirmed).","line":277,"path":"src/Treasury.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {Parameters} from \"src/Parameters.sol\";\nimport {Treasury} from \"src/Treasury.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract GasFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 days;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external pure returns (bool) {\n        return false;\n    }\n}\n\ncontract GasMirror is ISwarmFeed {\n    ISwarmFeed private immutable p;\n\n    constructor(ISwarmFeed p_) {\n        p = p_;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return p.latestValue();\n    }\n\n    function isStale() external view returns (bool) {\n        return p.isStale();\n    }\n\n    function maxAge() external view returns (uint256) {\n        return p.maxAge();\n    }\n}\n\ncontract GasAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\n/// @dev sIMD shape: 24 decimals over an 18-decimal asset; rate = asset raw per 1e18 share raw.\ncontract GasShare is ERC20 {\n    IERC20 public immutable underlying;\n    uint256 public constant rate = 7.95e12;\n\n    constructor(IERC20 a) ERC20(\"sIMD\", \"sIMD\") {\n        underlying = a;\n    }\n\n    function decimals() public pure override returns (uint8) {\n        return 24;\n    }\n\n    function asset() external view returns (address) {\n        return address(underlying);\n    }\n\n    function convertToAssets(uint256 shares) external pure returns (uint256) {\n        return shares * rate / 1e18;\n    }\n\n    function maxWithdraw(address owner) external view returns (uint256) {\n        return balanceOf(owner) * rate / 1e18;\n    }\n\n    function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) {\n        require(msg.sender == owner, \"owner only\");\n        shares = (assets * 1e18 + rate - 1) / rate;\n        _burn(owner, shares);\n        underlying.transfer(receiver, assets);\n    }\n\n    function deposit(uint256 assets, address receiver) external returns (uint256 shares) {\n        underlying.transferFrom(msg.sender, address(this), assets);\n        shares = assets * 1e18 / rate;\n        _mint(receiver, shares);\n    }\n}\n\ncontract GasToken is ERC20 {\n    constructor() ERC20(\"R\", \"R\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @dev A well-formed price source at listing time; once armed, every read spends all the gas it is given.\ncontract GasBurnFeed {\n    uint256 public constant maxAge = 1 days;\n    uint256 private immutable price;\n    bool public armed;\n\n    constructor(uint256 p) {\n        price = p;\n    }\n\n    function arm() external {\n        armed = true;\n    }\n\n    function isStale() external view returns (bool) {\n        if (armed) _burn();\n        return false;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        if (armed) _burn();\n        return (price, uint64(block.timestamp));\n    }\n\n    function _burn() private pure {\n        uint256 x;\n        while (true) {\n            x = x + 1;\n        }\n    }\n}\n\n/// @notice Treasury._boundedCall forwards gas() to a listed price source. A source that answers well at\n/// listing and later burns the gas it is given makes every cash (and earn) that reads the reserve cost\n/// ~63/64 of the transaction's gas: at an ordinary 3,000,000 gas limit the redemption reverts out of gas,\n/// where the same redemption cost ~300,000 gas before the feed turned, for the 48 hours a delisting takes.\ncontract Proof_BoundedCallGas is Test {\n    uint256 private constant IMD_ETH = 0.0005 ether; // $1 at ETH 2000\n    address private constant BORROWER = address(0xBA);\n    address private constant REDEEMER = address(0xCA);\n\n    MockIMD private imd;\n    GasShare private share;\n    GasFeed private primary;\n    GasFeed private nhi;\n    ParameterizedVault private vault;\n    Parameters private params;\n    Treasury private treasury;\n    ImdUSD private stable;\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        vm.roll(20_000_000);\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new GasAggregator()).code);\n        imd = new MockIMD();\n        share = new GasShare(imd);\n        primary = new GasFeed(IMD_ETH);\n        nhi = new GasFeed(0.85 ether);\n        vault = new ParameterizedVault(\n            address(share), address(0), address(0), address(primary), address(nhi), address(new GasMirror(primary))\n        );\n        params = vault.parameters();\n        treasury = vault.treasury();\n        stable = vault.stablecoin();\n    }\n\n    function _apply() private {\n        vm.warp(params.pendingEta());\n        primary.set(IMD_ETH);\n        nhi.set(0.85 ether);\n        vm.prank(address(0xA990));\n        params.applyPending();\n    }\n\n    function test_aListedFeedThatBurnsGasDoesNotMakeCashUnaffordable() public {\n        // A listing the register accepts: well-formed answers, a token with decimals.\n        GasToken junk = new GasToken();\n        GasBurnFeed feed = new GasBurnFeed(1 ether);\n        vm.prank(APPROVED_OPERATOR);\n        params.proposeReserveAsset(IERC20(address(junk)), ISwarmFeed(address(feed)), 10_000);\n        _apply();\n        junk.mint(address(treasury), 100 ether);\n        assertEq(treasury.reserveValueUsd(), 100 ether, \"listed and counted while healthy\");\n\n        // A borrower, some supply in a redeemer's hands, a reserve of sIMD, and a day for the lag.\n        vm.prank(APPROVED_OPERATOR);\n        imd.mint(BORROWER, 4000 ether);\n        vm.startPrank(BORROWER);\n        imd.approve(address(vault), 4000 ether);\n        vault.lockIMD(4000 ether);\n        vault.draw(1000 ether);\n        stable.transfer(REDEEMER, 100 ether);\n        vm.stopPrank();\n        vm.prank(APPROVED_OPERATOR);\n        imd.mint(address(this), 100 ether);\n        imd.approve(address(share), 100 ether);\n        share.deposit(100 ether, address(treasury));\n        vm.warp(block.timestamp + 1 days);\n        vm.roll(block.number + 1);\n        primary.set(IMD_ETH);\n        nhi.set(0.85 ether);\n\n        // The feed turns after listing (an upgradeable or externally controlled source).\n        feed.arm();\n\n        // EXPECTED (Treasury NatSpec: a dead source \"counts for nothing\" and \"never makes this view revert\"):\n        // the redemption still costs what it did, about 300,000 gas, so a 3,000,000 limit is ample.\n        // ACTUAL on this code: the staticcall forwards gas(), the feed burns 63/64 of it, and cash reverts.\n        vm.prank(REDEEMER);\n        (bool ok,) = address(vault).call{gas: 3_000_000}(abi.encodeCall(vault.cash, (10 ether, 0, address(0))));\n        assertTrue(ok, \"cash must not run out of gas because a listed feed burns the gas it is forwarded\");\n    }\n}","reproduction":"test/scratch/Proof_50bf84642e46.t.sol (FAILS on this code: 'cash must not run out of gas because a listed feed burns the gas it is forwarded'; PASSES with staticcall(200000, ...)). ParameterizedVault over a 24-decimal share of IMD (rate 7.95e12), IMD $1 (primary 5e14 x ETH/USD 2000 etched at CHAINLINK_ETH_USD), NHI 0.85, TreasuryFactory etched at TREASURY_FACTORY. Governor proposeReserveAsset(junk ERC-20, GasBurnFeed answering (1e18, now) and isStale false, 10000); +48h applyPending; junk.mint(treasury, 100e18): reserveValueUsd() == 100e18. Borrower lockIMD(4000e18), draw(1000e18), sends 100e18 imdUSD to a redeemer; 100 IMD deposited as shares to the Treasury; +1 day. feed.arm() (every read now loops until out of gas). EXPECTED (Treasury NatSpec 206-208, 267-272): the junk counts for nothing and cash(10e18, 0, 0) costs about 300,000 gas, so a 3,000,000 limit is ample. ACTUAL: address(vault).call{gas: 3_000_000}(cash(10e18, 0, 0)) returns ok == false (out of gas inside the forwarded staticcall, then the 1/64 remainder cannot finish the redemption); at 30,000,000 it succeeds using 29,270,102 gas. Second shape, test/scratch/Proof_f2a455eb1009.t.sol (both tests FAIL here, PASS with the stipend): one listing against a feed whose isStale() executes INVALID when armed: reserveValueUsd{gas: 5_000_000}() succeeds but uses 4,927,677 gas; three such listings: reserveValueUsd{gas: 30_000_000}() returns ok == false.","severity":"low","snippet":"            success := staticcall(gas(), target, add(data, 32), mload(data), out, 64)","title":"Treasury._boundedCall bounds the returndata copy but forwards gas(), so one listed source that burns gas puts a multi-million-gas floor under every cash, earn and backingPerUnit, and three such listin"},{"citation":"resolved","description":"Q4 (rights stranded wrongly across a replacement and a wage cycle). SwarmWorkOracle prices and records rights AT CLAIM (creditedTasks, creditedRights, lines 168-172); a claim needs only a nonzero wage, an accepted root and the controller. _validate's only measure of state to carry over is vault.totalEarned(), which moves only when earn consumes rights, and earn is often refused between a claim and a mint (WorkCeilingReached with an empty register and little warmed debt, or StaleFeed), so 'claimed, not yet minted' is an ordinary state. The documented replacement route then REQUIRES governance to zero the wage (WorkMintingOn), and at wage 0 nothing can be minted (the 8756817 _earnOpen gate), so the holder has no exit during the 96 hours of public proposals. Once the successor is applied: vault.oracle() is the successor, earn reverts InsufficientRights (the successor holds nothing), the old oracle's claim reverts NotTheVaultsOracle (the 8756817 info fix) while its mintingRights still reports the credit, and only the vault may consumeRights, so the rights priced under the old wage are unconsumable forever. In the successor creditedTasks starts at zero, so the SAME cumulative tally is claimable again, at whatever wage governance sets next, by whoever controls the agent NOW, and only against a root the successor has accepted, which needs a new attestation bought for its address; a daily receipt lists only agents that worked that day (SwarmWorkOracle.sol:71-75), so an agent who has stopped working never recovers. The agent loses the price their work was credited at (1.0 to 0.1 imdUSD per task in the specialist's run), and if the identity NFT changed hands the credit for work done under the previous controller moves to the buyer, contradicting claim's own split rule (lines 146-149). No double CONSUMPTION is possible (the vault reads one oracle; a return to the old one is itself a WorkOracle proposal refused once anything was minted), so this is a governance-visible loss, not a bypass. Reachable with the constants as committed after one ordinary wage cycle; never on day one (WAGE_WAD = 0, no claims). NatSpec claiming a property the code does not have: Parameters.sol:252-255 ('so it can start from the tallies already credited... before that, there is nothing to carry over'), ParameterizedVault.sol:122-123 ('Rights claimed under a wage are kept, and spendable again the moment a wage is set'), docs/MAINNET-RUNBOOK.md 7b.3 ('before the first mint, a fresh SwarmWorkOracle built directly for the vault... is proposable'). Smallest fix: treat outstanding credited rights like a mint. Add `uint256 public totalCredited` to SwarmWorkOracle (+= rights in claim, -= amount in consumeRights) and in _validate probe vault.oracle() with a raw staticcall for totalCredited(); if it answers a nonzero word, require successor.predecessor() == vault.oracle() exactly as for `minted` (MockWorkOracle, which does not answer, is unaffected). Trade-off to state: this closes replacement at the first CLAIM rather than the first mint, which is the documented 'cannot be replaced until a predecessor-carrying type exists' position moved one step earlier, and it is within the governor's control (no claims exist until the governor sets a wage). If the requester prefers to keep the replacement open, the NatSpec at Parameters.sol:252-255 and ParameterizedVault.sol:122-123 and runbook 7b.3 must instead say that claims made under an earlier wage are stranded by a replacement and must be re-claimed, at the new wage and by the current controller, against a root the successor accepts. Merged from audit_permissions and audit_flow (both reproduced; the flow specialist's re-credit-to-buyer consequence is folded in).","line":400,"path":"src/Parameters.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {Parameters} from \"src/Parameters.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {SwarmWorkOracle} from \"src/SwarmWorkOracle.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {\n    APPROVED_OPERATOR,\n    CHAINLINK_ETH_USD,\n    TREASURY_FACTORY,\n    WORK_ORACLE_FACTORY,\n    WORK_ORACLE_SENTINEL,\n    ERC8004_ADAPTER\n} from \"src/DeploymentConfig.sol\";\n\ncontract SrFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 days;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external pure returns (bool) {\n        return false;\n    }\n}\n\ncontract SrMirror is ISwarmFeed {\n    ISwarmFeed private immutable p;\n\n    constructor(ISwarmFeed p_) {\n        p = p_;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return p.latestValue();\n    }\n\n    function isStale() external view returns (bool) {\n        return p.isStale();\n    }\n\n    function maxAge() external view returns (uint256) {\n        return p.maxAge();\n    }\n}\n\ncontract SrAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract SrShare is ERC20 {\n    IERC20 public immutable underlying;\n    uint256 public constant rate = 7.95e12;\n\n    constructor(IERC20 a) ERC20(\"sIMD\", \"sIMD\") {\n        underlying = a;\n    }\n\n    function decimals() public pure override returns (uint8) {\n        return 24;\n    }\n\n    function asset() external view returns (address) {\n        return address(underlying);\n    }\n\n    function convertToAssets(uint256 shares) external pure returns (uint256) {\n        return shares * rate / 1e18;\n    }\n\n    function maxWithdraw(address owner) external view returns (uint256) {\n        return balanceOf(owner) * rate / 1e18;\n    }\n\n    function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares) {\n        require(msg.sender == owner, \"owner only\");\n        shares = (assets * 1e18 + rate - 1) / rate;\n        _burn(owner, shares);\n        underlying.transfer(receiver, assets);\n    }\n\n    function deposit(uint256 assets, address receiver) external returns (uint256 shares) {\n        underlying.transferFrom(msg.sender, address(this), assets);\n        shares = assets * 1e18 / rate;\n        _mint(receiver, shares);\n    }\n}\n\n/// @dev The production work oracle with a test-only seeding door (the attester's key is not ours).\ncontract SrSeedableWork is SwarmWorkOracle {\n    constructor(address vault_, uint256 maxAge_) SwarmWorkOracle(vault_, maxAge_) {}\n\n    function seed(uint256 v) external {\n        _accept(v, uint64(block.timestamp));\n    }\n}\n\ncontract SrSeedableWorkFactory {\n    function create(uint256 maxAge_) external returns (SrSeedableWork o) {\n        o = new SrSeedableWork(msg.sender, maxAge_);\n    }\n}\n\n/// @notice Parameters.proposeWorkOracle keys \"there is nothing to carry over\" on vault.totalEarned() alone.\n/// Rights are credited at claim, so an oracle can hold credited, unconsumed rights with nothing minted; a\n/// fresh SwarmWorkOracle is then accepted as the successor and those rights are stranded: the old oracle\n/// refuses further claims (NotTheVaultsOracle), the vault reads the new one (InsufficientRights).\ncontract Proof_StrandedRights is Test {\n    uint256 private constant IMD_ETH = 0.0005 ether;\n    uint256 private constant AGENT = 51450;\n    address private constant CONTROLLER = address(0xA11CE);\n\n    MockIMD private imd;\n    SrFeed private primary;\n    SrFeed private nhi;\n    ParameterizedVault private vault;\n    Parameters private params;\n    SrSeedableWork private work;\n\n    function setUp() public {\n        vm.warp(1_700_000_000);\n        vm.roll(20_000_000);\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(WORK_ORACLE_FACTORY, address(new SrSeedableWorkFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new SrAggregator()).code);\n        imd = new MockIMD();\n        SrShare share = new SrShare(imd);\n        primary = new SrFeed(IMD_ETH);\n        nhi = new SrFeed(0.85 ether);\n        vault = new ParameterizedVault(\n            address(share), address(0), WORK_ORACLE_SENTINEL, address(primary), address(nhi), address(new SrMirror(primary))\n        );\n        params = vault.parameters();\n        work = SrSeedableWork(address(vault.oracle()));\n    }\n\n    function _apply() private {\n        vm.warp(params.pendingEta());\n        primary.set(IMD_ETH);\n        nhi.set(0.85 ether);\n        vm.prank(address(0xA990));\n        params.applyPending();\n    }\n\n    function _setWage(uint256 wad) private {\n        vm.prank(APPROVED_OPERATOR);\n        params.proposeWage(wad);\n        _apply();\n    }\n\n    /// @dev A two-leaf StandardMerkleTree holding the agent's leaf and one other.\n    function _tree(uint256 id, uint32 accepted, uint64 cumulative) private pure returns (bytes32 root, bytes32[] memory proof) {\n        bytes32 a = keccak256(bytes.concat(keccak256(abi.encode(id, accepted, cumulative))));\n        bytes32 b = keccak256(bytes.concat(keccak256(abi.encode(uint256(1), uint32(1), uint64(1)))));\n        root = a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a));\n        proof = new bytes32[](1);\n        proof[0] = b;\n    }\n\n    function test_aReplacementIsRefusedWhileTheCurrentOracleHoldsCreditedUnconsumedRights() public {\n        // Before any claim a fresh successor is a valid proposal (nothing to carry over), and is cancelled.\n        SwarmWorkOracle early = new SwarmWorkOracle(address(vault), 1 days);\n        vm.prank(APPROVED_OPERATOR);\n        params.proposeWorkOracle(address(early));\n        vm.prank(APPROVED_OPERATOR);\n        params.cancel();\n\n        // Governance turns the wage on; an agent's controller claims 500 attested tasks and mints nothing.\n        _setWage(1 ether);\n        (bytes32 root, bytes32[] memory proof) = _tree(AGENT, 500, 500);\n        work.seed(uint256(root));\n        work.recordRoot();\n        vm.mockCall(\n            ERC8004_ADAPTER, abi.encodeWithSignature(\"isController(uint256,address)\", AGENT, CONTROLLER), abi.encode(true)\n        );\n        vm.prank(CONTROLLER);\n        assertEq(work.claim(AGENT, 500, 500, proof, root), 500 ether);\n        assertEq(work.mintingRights(CONTROLLER), 500 ether, \"credited, unconsumed\");\n        assertEq(vault.totalEarned(), 0, \"nothing minted\");\n\n        // Governance turns the wage off, as proposeWorkOracle requires, and proposes a fresh SwarmWorkOracle.\n        _setWage(0);\n        SwarmWorkOracle successor = new SwarmWorkOracle(address(vault), 1 days);\n        // EXPECTED: refused, because the current oracle still carries credited rights the successor cannot\n        // carry over (the same rule that applies once anything has been minted).\n        // ACTUAL on this code: accepted; applied 48 hours later the 500e18 rights are stranded in the old\n        // oracle (it refuses claims once superseded; the vault reads the successor, which holds nothing).\n        vm.prank(APPROVED_OPERATOR);\n        vm.expectRevert();\n        params.proposeWorkOracle(address(successor));\n    }\n}","reproduction":"test/scratch/Proof_3bef3812020d.t.sol (FAILS on this code with 'next call did not revert as expected'; PASSES with the totalCredited gate). ParameterizedVault built with WORK_ORACLE_SENTINEL (WORK_ORACLE_FACTORY etched to a factory building a SwarmWorkOracle subclass whose only addition is a seeding door for the root; claim/rights/consume/wage are production code), 24-decimal share collateral, IMD $1, NHI 0.85. Governor proposeWage(1e18), +48h applyPending. Two-leaf root over (51450, 500, 500) seeded and recordRoot(); ERC8004_ADAPTER mocked to confirm CONTROLLER; CONTROLLER claim(...) returns 500e18; work.mintingRights(CONTROLLER) == 500e18; vault.totalEarned() == 0. Governor proposeWage(0), +48h apply; B = new SwarmWorkOracle(address(vault), 1 days); governor proposeWorkOracle(B). EXPECTED per Parameters.sol:254-255: refused, as it is after a first mint, because the current oracle carries credited rights the successor cannot carry. ACTUAL: accepted. Full chain in test/scratch/JudgeStranded.t.sol (PASSES as a demonstration): with 10,000e18 of warmed debt, after +48h apply (vault.oracle() == B) and proposeWage(1e18) +48h: earnLine() > 1e18, work.mintingRights(CONTROLLER) == 500e18, B.mintingRights(CONTROLLER) == 0, vault.earn(1e18) from CONTROLLER reverts InsufficientRights, work.claim(...) reverts NotTheVaultsOracle, and B.claim(...) by the agent's new controller reverts UnknownRoot until an attestation is bought for B.","severity":"low","snippet":"            bool minted = vault.totalEarned() != 0;","title":"Parameters.proposeWorkOracle keys 'nothing to carry over' on totalEarned alone, so a fresh SwarmWorkOracle without predecessor() is accepted while the current oracle holds rights credited at claim but"},{"citation":"resolved","description":"Q5 (is the statement complete and are the bounds as described). The mechanisms are stated correctly and the code matches: _validate enforces every one; every proposal waits TIMELOCK = 48 hours (Governed.sol:25, 58, 83), one slot at a time (_propose reverts ProposalPending while pendingEta != 0), _validate runs again at application; reserveValueOf caps each asset at MAX_RESERVE_VALUE (Treasury.sol:238) and saturates the sum; earn is refused at wage 0 (ParameterizedVault._earnOpen, CDPVault.earn:491); a hostile oracle cannot be installed while the wage is nonzero and cannot replace a SwarmWorkOracle after a first mint. Three things the statement does not say. (1) Hours: 'after 96 hours' counts the listing and the oracle; the same paragraph then says the oracle step mints nothing until a wage proposal has also been public 48 hours, so from the launch state (wage 0) the path it describes is three serial proposals, 144 hours, and from a running wage four (wage to 0 first), 192; the oracle + wage pair alone (96 hours) mints only against the ratio term (backedDebt x earnMat / 10000, up to 25% of the lagged collateral-backed debt, $250k at the $1M LINE). The two sentences give different totals for the same path. (2) 'The per-asset cap' is Treasury.MAX_RESERVE_VALUE = 1e36 in 1e18-scaled USD, i.e. $1e18 per listed asset, a trillion times LINE ($1e6), with _reserveAssets unbounded in length and earn reading no `line`: its own NatSpec (Treasury.sol:63) says it is an overflow bound, not a limit on damage. (3) The listing step has a consequence of its own that needs neither oracle nor wage and is the opposite direction from 'lowering the redemption backing': reserveValueUsd is the `others` term of ParameterizedVault._redemptionReserveBacking (ParameterizedVault.sol:178), which CDPVault._backingPerUnit adds on both the live and the lagged side for every cash. A single listing of a token the Treasury holds one unit of, against a feed answering 1e30, after 48 hours reads backingPerUnit() at par whatever the collateral is worth, and any unprivileged redeemer (including one who bought below peg) is then paid (1 - fee) of par in real sIMD from the Treasury's reserve first and from candidates' collateral next, while true backing is below par; the remaining holders are left with the junk. Measured: at backing 0.84 after a price fall, cash(10e18) paid 20.79 IMD; after the listing alone, 24.75 IMD (1.19x), with backingPerUnit() reading 1e18. The ParameterizedVault header ('See Parameters for what that lets the governor do') defers to the same incomplete text, and runbook section 3 does not mention it. This is the governor's intended, visible power and is reported as the completeness of the statement, as the final panel classed the whole power (info), not as a bypass. Smallest fix, documentation only: restate lines 54-62 as 'a reserve listing sets the reserve term of earnLine AND of the redemption backing (backingPerUnit, cash), so a listing alone, after 48 hours, lets redeemers be paid at par against a reserve the governor priced; with a replacement oracle (48 h) and a wage (48 h), 144 hours in all from launch, it is also minting with no collateral and no attested work; against the ratio term alone the oracle and wage steps, 96 hours, mint up to earnMat of the lagged backed debt; the per-asset cap is $1e18 and bounds overflow, not damage', and say the same in runbook section 3. If the redemption half is not intended, exclude registered non-collateral assets from _redemptionReserveBacking instead (they never leave through redeemIMD). Merged from audit_permissions, audit_economics, audit_flow and audit_math (one statement, four reproductions, all confirmed; the economics specialist's 'low' is kept at the panel's 'info' because the fix is to the text and the power is the stated design).","line":58,"path":"src/Parameters.sol","reproduction":"Hours, read-only: Governed.TIMELOCK = 48 hours; Governed._propose reverts ProposalPending while pendingEta != 0, so proposeReserveAsset -> applyPending -> proposeWorkOracle -> applyPending -> proposeWage -> applyPending cannot complete before 3 x 48 = 144 hours; Parameters.sol:58 says 96. Cap: Treasury.sol:64 MAX_RESERVE_VALUE = 1e36 (1e18-scaled USD) against DeploymentConfig.LINE = 1_000_000e18: 1e36 / 1e24 = 1e12. Payout: test/scratch/JudgeListing.t.sol (PASSES as a demonstration). WorkBackingFixture (OpenWorkVault over MockIMD at $1, NHI 0.85, empty register). BORROWER locks 2,000 IMD and draws 1,000 imdUSD, 100 imdUSD to REDEEMER, 100 IMD minted to the Treasury as its reserve; one day warms the lag; the price falls to $0.40: backingPerUnit() == 840000000000000000 ((800 + 40) / 1000). cash(10e18, 0, BORROWER) pays 20790000000000000000 raw collateral (snapshot, reverted). Then APPROVED_OPERATOR proposeReserveAsset(ReserveTestToken holding 1e18 at the Treasury, TestSwarmFeed answering 1e30, 10000), +48h applyPending; no oracle or wage change. EXPECTED per the statement: the listing sets only 'the reserve term of earnLine' and the governor's power is 'lowering the redemption backing'. ACTUAL: reserveValueUsd() == 1e30, backingPerUnit() == 1e18, and the same cash(10e18, 0, BORROWER) pays 24750000000000000000 raw collateral out of the vault's real reserve.","severity":"info","snippet":"/// Together, after 96 hours of public proposals, that is minting with no collateral and no attested","title":"The governor's minting trust statement in Parameters is incomplete: the three-step path it describes is 144 hours with one proposal slot (not 96), the 'per-asset cap' is $1e18 per listing with an unbo"},{"citation":"resolved","description":"Q2 and Q6, a gap left by the 8756817 fix for the final governance panel's low #2 ('spend plain IMD first'). When the Treasury's plain IMD covers only part of the day's remaining budget (plain < want), the remainder is unwrapped from sIMD in the SAME call with no fallback: _withdraw(imd, ORACLE_ASKER, plain) is followed by _withdrawUnderlying -> IShareVault.withdraw, and sIMD refuses that (SameBlockRedeem) in any block in which the Treasury received shares (the hold travels with a transfer: docs/COMPUTE-BACKING-DESIGN.md:537-548). So in a block where a liquidation's protocol cut lands, or where anyone sends one raw share unit to the Treasury (accepted residual D5, docs/AUDIT-FIX-PLAN-2026-10-05.md:38), the whole call reverts and the plain IMD, which needs no unwrap, is not sent. The NatSpec at Treasury.sol:488-490 documents the revert for the share case and the honest case clears a block later; what the fix promised (487-488 and runbook 7.4: plain IMD revenue 'takes over' from the keeper) is nevertheless not delivered in exactly the D5 state, which D5's acceptance ('a later block succeeds') did not weigh for the plain path because it did not exist. Bounded: griefing or a one-block delay, never loss; the keeper fallback and askPaid revive it, so info rather than low. CAVEAT: sIMD's hold is modelled from the repository's own fork notes (test/SharePriceFeedFork.t.sol:168-176, COMPUTE-BACKING-DESIGN.md:537-548); StakedIMD itself could not be reached here. Smallest fix: make the share leg best-effort: send `plain` first, then `try` the unwrap through an external self-call (`this.unwrapForOracle(fromShares)`, msg.sender == address(this)) and on failure set fromShares = 0, computing `sent` and `oracleSpent` from what actually left; or return early with sent = plain when the share vault reports a hold for this block. Merged from audit_economics and audit_flow (both reproduced against a modelled hold).","line":533,"path":"src/Treasury.sol","reproduction":"test/scratch/JudgeFundOracleHold.t.sol (PASSES as a demonstration). ParameterizedVault over HeldShare: an ERC-4626-shaped 24-decimal share (rate 7.95e12) that stamps lastIn[receiver] = block.number on every mint or transfer and reverts SameBlockRedeem in withdraw while lastIn[owner] == block.number; ORACLE_ASKER etched with code; default oracleBudget 15e18. The Treasury holds 10e18 plain IMD (below the budget) and 50e18 IMD deposited as shares in an earlier block. In the keeper's block anyone deposits 1e18 IMD into the share and transfers one raw share unit to the Treasury. treasury.fundOracle(): EXPECTED (NatSpec 487-488, runbook 7.4): at least the 10e18 plain IMD reaches the asker. ACTUAL: reverts SameBlockRedeem; imd.balanceOf(ORACLE_ASKER) == 0. One block later fundOracle() returns 15e18 (10 plain + 5 unwrapped) and the asker holds 15e18.","severity":"info","snippet":"            if (share) _withdrawUnderlying(IERC20(token), fromShares);","title":"fundOracle's new plain-IMD leg is all-or-nothing with the sIMD unwrap it falls through to: when the share leg reverts under sIMD's same-block hold (a liquidation cut, or the accepted D5 dust transfer,"}],"hash":"f48efc79a11f8b8929d4ec56751ce35d81de1119790bd0440a5126df678ea6f3","nodeId":"8919c370-3c76-4efe-8a67-b4d83728071b","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds four findings (2 low, 2 info). No tracked file was changed: the one temporary patch to `Treasury.sol` used to validate the gas fix was reverted, and only `test/scratch/` and the findings file are new.\n\n## Judgement of the specialist findings\n\n| Specialist claim | Verdict |\n|---|---|\n| `_boundedCall` forwards `gas()` so a gas-burning listed source prices out or shuts cash, earn and backingPerUnit (4 × low) | **Confirmed, merged into one low.** All three attached proofs fail here for the stated reason and pass with a 200k stipend. 122 existing tests stay green with that change. |\n| `proposeWorkOracle` strands rights claimed but not yet minted (2 × low) | **Confirmed, merged into one low.** The attached proof fails here. My own full-chain test shows the successor installed, `earn` refusing `InsufficientRights`, the old oracle refusing `NotTheVaultsOracle`, and the successor refusing `UnknownRoot`. I added the trade-off the specialists' fix carries: it closes replacement at the first claim rather than the first mint. |\n| Trust statement incomplete: 144 not 96 hours, $1e18 per-asset cap, listing alone lifts the redemption payout (3 × info, 1 × low) | **Confirmed, merged into one info.** Measured 20.79 → 24.75 IMD paid for 10 imdUSD after a listing alone. Kept at the panel's info level since the fix is to the text and the power is the stated design. |\n| fundOracle's plain leg lost when the share leg is held (1 × low, 1 × info) | **Confirmed, merged into one info** with the caveat that the hold is modelled from the repository's fork notes. |\n| audit_permissions coverage record (info, \"not a defect\") | **Dropped as a finding.** Its content is folded into the answers below. |\n\nNothing the specialists reported failed to reproduce.\n\n## Answers to the six questions\n\n1. **Timelock and bounds.** No change lands sooner than 48 hours, outside a bound, or by another route. One slot, `onlyGovernor` on propose and cancel, `_validate` at both ends, every bound a constant. The only third-party block on a WorkOracle proposal was closed by the wage-0 earn gate. Post-eta chosen-moment application is the accepted note in `Governed.sol`.\n2. **Treasury exits.** All six are enumerated and bounded as the code says. Day boundaries reset on a new UTC day, a lowered budget pays nothing more, a raised one pays the difference. Plain-IMD accounting through `_withdraw` credits arrivals before moving the baseline and re-derives it after. The one gap is the all-or-nothing coupling to the share leg (info).\n3. **Reserve valuation.** The assembly is memory-safe: 64 bytes at the free pointer without advancing it is permitted temporary memory, and `first`/`second` are read only when `returndatasize` allows. A reverting or short-answering token or feed counts for nothing. The remaining resource is gas (low).\n4. **Work oracle.** Wage 0 at both ends holds, direct construction qualifies, `predecessor()` is required after a mint, and the `oracle()` probe returns true for a vault with no `oracle()`. The gap is state between claim and mint (low).\n5. **Trust statement.** Mechanisms and the 48-hour bounds are as described. The statement understates the hours and omits the redemption-backing consequence of a listing (info).\n6. **Day one.** No dead state without a revival: the seed, the keeper fallback, donations to the asker, and operator withdraw of unlisted IMD all exist and runbook section 7 names the keeper.\n\n## Coverage\n\nRead in full: Parameters, Governed, Treasury, TreasuryFactory, WorkOracleFactory, SwarmWorkOracle, ParameterizedVault, CDPVault, DeploymentConfig, SharePriceFeed, UsdPriceFeed, MockWorkOracle, the IShareVault, IWorkOracle and ISwarmFeed interfaces, the fix diff of 8756817, the final governance panel report, and runbook sections 3, 7, 7b and 8. Could not reach: StakedIMD itself, the live Chainlink aggregator, the ERC-8004 adapter and the Intake. No static analyser was run.","treeHash":null,"usage":{"cachedInputTokens":2671207,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":38069,"runtime":"claude","turns":37,"wallClockMs":560624}}],"verification":[]}