{"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":"ecd0a279-c5d7-4167-8e75-9fbbc85b41fe","kind":"audit","nodes":[{"acceptedSubmissionHash":"bdbc9d0066821066303a092e874ff3008393a9aced7d33df26d8e85347877c41","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":"625a6c7abd63f664ab5799b0287306cbe5c8f457c4e971afc5b2626665578fc3","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":"1db7293d2481b80a187762d1d5323701628e86b6e65bb71a6ad0463aa639f5a8","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":"f0c636da07c3ce9409b782cadf84b9802de2c172b78a153d973305785197b2b8","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":"0060598dbda118de23e6bd356c5b4224d2322725e236ff8288ca4a7d46847184","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 the price path: src/SwarmFeed.sol, src/PriceFeed.sol, src/NhiFeed.sol, src/SpotFeed.sol, src/SwarmRelay.sol, src/OracleAsker.sol, src/UsdPriceFeed.sol, src/SharePriceFeed.sol, and script/DeployMainnet.s.sol's verify and verifySeeded, 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. Attestation acceptance: signature domain, replay, issuedAt freshness, panel floors, question binding over the window. Any attestation for a different question, chain, feed or window accepted?\n2. The per-epoch bound as committed: the epoch's anchor and stored allowance; _allowanceNow (the cap while fresh and through the first hour of silence, measured from the LATER of the signature and the relay (_acceptedAt); then 2x the cap plus an eighth per further hour, capped at MAX_ALLOWANCE_BPS); a wide epoch holding later values to the cap around its first (_epochFirst, uint80). Slot 3 is repacked (uint64 updatedAt, bool, uint40 anchorAt, uint24 bound, uint40 acceptedAt, uint80 epochFirst): check the packing and every cast. State the fastest sustained rate a buyer who chooses issue times, relay times and windows can drive the price, in both directions and on the one-day NHI feed, and the largest single step after H hours of silence; find any sequence faster than the cap per hour after the first step, or any genuine gap never followed.\n3. OracleAsker: ask (keep-alive near stale; wideOpen = stale, no live epoch, allowance >= WIDE_ALLOWANCE_BPS; an armed fall), askPaid, askPaidMany, _request (a timed-out request keeps feedOf), onOracleResult (back-off only when the live request was the Treasury's own: lastAsk == inFlightAt). Can anyone make the Treasury pay without cause, exceed the daily budget, hold it off, lose a paid answer, or fake the Treasury-paid test?\n4. The first value: unbounded on chain, relayed by anyone; DeployMainnet.verifySeeded() (price and spot within a quarter of the cap of the pool, of each other within SKEW_BPS, NHI in (0, 1e18]) and runbook 7.1. Is the check sufficient to keep a raced first value from pricing any deposit, and what does a failed check cost?\n5. Price composition and dead legs: UsdPriceFeed, SharePriceFeed, a reverting or malformed Chainlink or share-vault leg, and what each consumer (vault actions, the ungated paths, the Treasury's reserve) does with it.\n6. The walk, costed at the committed constants against LINE $1M and mat 170, both directions, including the hold-then-relay variant: cost, earnings, and what stops 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":"33a00f7af97edee789f443a539fa376722f907fd26ef416f83c4603b5e13ee3a","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"ecd0a279-c5d7-4167-8e75-9fbbc85b41fe","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":"52182","feedbackHash":"e3ff7999f77a7062b77db868322f9740982f73b746d9392649b034da3acc5c09","nodeKey":"audit_economics","submissionHash":"bdbc9d0066821066303a092e874ff3008393a9aced7d33df26d8e85347877c41","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52289","feedbackHash":"9add2fea47058045c8c00ec55326838ce7d3e568d3e4b7d8791e4e53d52b4c57","nodeKey":"audit_flow","submissionHash":"625a6c7abd63f664ab5799b0287306cbe5c8f457c4e971afc5b2626665578fc3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51481","feedbackHash":"ca848aa2243b83631f1fd51246bac1a4463eea4cc2ebcab6b70277abe9eca61b","nodeKey":"audit_judge","submissionHash":"1db7293d2481b80a187762d1d5323701628e86b6e65bb71a6ad0463aa639f5a8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50988","feedbackHash":"0d7d14472bb72b01db275a5a0e10ef111750432cf70ad91f24c43ef7249c4896","nodeKey":"audit_math","submissionHash":"f0c636da07c3ce9409b782cadf84b9802de2c172b78a153d973305785197b2b8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51474","feedbackHash":"6234e5ed69f594208c654b3091ac6ceef38bedb3ad9545a3b3d063f99dd45130","nodeKey":"audit_permissions","submissionHash":"0060598dbda118de23e6bd356c5b4224d2322725e236ff8288ca4a7d46847184","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"15c5e45fcd5a05f77b9a7e06cd92e67b3c2707e4fc58ac13ede6f5f4994ca64a","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0e3b71e2ffcd200b","findings":[{"citation":"resolved","description":"The 8756817 fix for the final panel's oracle low #2 identifies the Treasury's own purchase by `f.lastAsk == f.inFlightAt`, on the reasoning (lines 266-268) that only `ask` writes lastAsk and no request can follow it within its second. But the catch branch (line 293) also writes lastAsk, to a FUTURE time: `block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL` = now + 6600 s. 6600 is a multiple of 12, so on mainnet (every block timestamp is genesis + 12k) that value is itself a valid future slot time. A caller-paid `askPaid` (lines 180-190: no TooSoon check, in-flight slot clear after the callback) mined in that slot writes `inFlightAt = block.timestamp = lastAsk`, and when the Intake delivers it the callback computes treasuryPaid = true. If that answer is relayed by hand first (the attestation is public; SwarmRelay forwards for anyone) the callback's relay reverts ReplayedAttestation and the catch writes lastAsk = now + 6600 again. Repeated, each cycle costs the attacker 0.5 IMD (~$5) and keeps `ask` reverting TooSoon: the NHI keep-alive, an armed 5% fall and a wide-open refresh are never bought with protocol money, which is exactly the hold-off the fix was meant to close (the panel rated it low; the Treasury-paid path is the protocol's only unattended oracle funding, the keeper's own IMD is the fallback). The entry point into the chain is any genuine Treasury refusal: the attacker calls the permissionless `ask` when NHI is near stale (daily) and hand-relays the plane's answer before the callback lands, or waits for a refusal that happens on its own (an NHI index that moved past the epoch allowance). Reachable with the constants as committed: ASK_TIMEOUT 2h, ASK_MIN_INTERVAL 10 min, both multiples of 12 s; a missed slot only costs the attacker one cycle (wrap askPaid in a contract that reverts unless block.timestamp == target). NatSpec claim the code does not have: lines 266-268 ('`ask` writes lastAsk and inFlightAt in the same call, `askPaid` and `askPaidMany` write only inFlightAt, and no request can follow an `ask` within its second') and 288-293 ('only the Treasury's own live purchase holds the Treasury back'). Smallest fix: record who paid instead of inferring it from timestamps: add `bool treasuryPaid` to `Feed` (slot 1 has 6 spare bytes, so no new storage slot), set it true in `ask` and false in `askPaid`/`askPaidMany` next to the inFlightAt write, and test `live && f.treasuryPaid` in the catch. Alternatively write the back-off as `lastAsk = block.timestamp + 6600 + 1` is NOT enough (a timestamp one past a slot is unreachable today but ties correctness to the slot grid); the flag is the right fix.","line":273,"path":"src/OracleAsker.sol","reproduction":"test/scratch/FakeTreasuryPaid.t.sol, test_aTimedAskPaidReadsAsTreasuryPaidAndExtendsTheBackOff (fails on this code). Fixture: SwarmRelay at ATTESTATION_RELAYER, a mock Intake at INTAKE selling oracle.request for 0.5 IMD, OracleAsker over one keep-alive NHI feed (maxAge 1 day, cap 2000) seeded at 0.9e18, asker funded with 15 IMD, block.timestamp a multiple of 12. (1) T+18h: ATTACKER calls ask(nhi) -> R1 (lastAsk = inFlightAt = T1). (2) ATTACKER relays R1's signed attestation through SwarmRelay by hand, then the Intake completes R1: ReplayedAttestation, caught, lastAsk = T1' + 6600 (a genuine Treasury back-off). (3) vm.warp(lastAsk); ATTACKER askPaid(nhi, body, 0.5e18) -> R2; feeds(nhi).inFlightAt == lastAsk. (4) five minutes later ATTACKER hand-relays R2's attestation, the Intake completes R2. EXPECTED (per lines 266-273, 288-293): lastAsk unchanged at the step-2 value, since R2 was caller-paid. ACTUAL: lastAsk == block.timestamp + 6600 (assertion output '1700078300 != 1700071400'); the Treasury's balance is unchanged, so the request that backed it off cost it nothing. test_theChainHoldsTheTreasuryOffIndefinitely repeats step 3-4 twelve times (27 hours, 6 IMD of the attacker's): ask(nhi) still reverts TooSoon at the end.","severity":"low","snippet":"        bool treasuryPaid = live && f.lastAsk == f.inFlightAt;","title":"OracleAsker.onOracleResult: a caller-paid askPaid placed in the block whose timestamp equals the back-off lastAsk passes the Treasury-paid test (lastAsk == inFlightAt), so its refusal backs the Treasu"},{"citation":"resolved","description":"The 8756817 fix for the final panel's oracle low #3 (the unbounded first value, SwarmFeed._checkValue line 390 `if (_hasValue) {`) is a script check, and it has two gaps against the scenario it was written for. (1) It is not a gate. ParameterizedVault is live from its constructor: lock, draw, bark and bite all run as soon as the three feeds hold fresh values (CDPVault lines 382-481), and runbook 7.1 step 5 'only then open deposits' is an announcement with no on-chain switch. The first value of each feed is set by whoever relays first through the permissionless SwarmRelay (the feed addresses and the Intake bodies are public before the broadcast, so an attacker can buy attestations for the planned addresses and relay the moment the feeds exist), and in the hour that value is fresh anyone can draw against it up to LINE ($1M) at mat 170. verifySeeded, run minutes or hours later, can only report that this already happened. (2) The comparison is against `asker.poolPrice()` at the moment the script runs, i.e. the same manipulable pool the attestation was taken from. An attacker who holds the pool at 2x through the attestation windows and through the operator's own purchase and check passes it: price, spot and the pool all read 2x and agree within SKEW_BPS, and the operator's honest attestation reads 2x too. The first value is bounded by nothing, so this is the whole walk in one step: PARAMETERS costs the walk to 3.48x at about $40k of pool fees over six hours of holding the pool at rising rungs; at launch the same prize needs one rung held for about an hour (the two windows plus the script), with the pool's 1% fee each way on roughly 348 ETH (about $19k) plus exposure to holders selling into the pump. What stops it: there is no imdUSD market on day one to sell the proceeds into, and the operator may notice a pool at twice the price they read on 2026-10-05 (the script does not). Cost of a failed check: the honest value is refused ExcessDeviation until the allowance reaches the gap (a 2x anchor needs 5,000 bps: periods >= 5, six hours of silence after the raced relay; a 3x anchor 6,667 bps: periods >= 12, thirteen hours), the vault is stale and halted from hour one of that wait, and any draw made in the first hour against the raced value is bad debt from block one (a position at 2V is at 88.75% of mat-170 collateral at V). Nothing in the fix regressed; it left the on-chain gap open. Smallest fix, either: seed in the deployment transaction so no block exists in which a feed is unseeded (buy the three attestations for the planned addresses before the broadcast, relay them in `run()` right after the feeds deploy and before the vault), or add a minimal on-chain gate: ParameterizedVault reads `parameters.line()`, which is governed; ship with the constant LINE but have the deployment propose it only after verifySeeded passes (the Parameters default line of 0 refuses every draw until then; a 48-hour wait is the cost). For the check itself, compare against a reference the attacker does not hold: a pool TWAP over the hours before the deployment, or the operator's off-chain reference price passed in as an argument, not the live slot0 at the moment of the check.","line":360,"path":"script/DeployMainnet.s.sol","reproduction":"test/scratch/RacedFirstValuePricesADraw.t.sol (passes on this code: it demonstrates the state). Three feeds built on SwarmFeed with cap 2000 and SwarmRelay at ATTESTATION_RELAYER, a CDPVault over them (open from its constructor, same gating as ParameterizedVault), V = 3.55e15 the market. STRANGER relays first values through SwarmRelay: price 2V, spot 2V, NHI 0.9e18 (accepted: `_checkValue` applies no bound while !_hasValue, epoch anchor 2V). Same block STRANGER locks 1,000e18 units and draws 4e18 (max 4.17e18 at mat 170): EXPECTED per runbook 7.1 / verifySeeded NatSpec ('refuse a first value someone else raced in ... before deposits open') no deposit priced off it; ACTUAL totalDebt == 4e18, the position is at 88.75% CR at the market price. DEPLOYER relays the honest V: ExcessDeviation. At +5h59m still ExcessDeviation (allowance 4,750); at +6h00m01s accepted (allowance 5,000). verifySeeded run against this state with the pool restored to V fails as designed; run while the stranger still holds the pool at 2V, `_within(2V, 2V, 500)` and `_within(2V, 2V, 500)` both pass and the check reports 'Seeded and verified'.","severity":"low","snippet":"        require(_within(price, pool, band), \"seeded: the price feed's first value is off the pool: do not open deposits\");","title":"DeployMainnet.verifySeeded compares the first values against the same live pool the attacker holds, off chain and after the fact; nothing on chain gates a deposit on it, so a raced (or simply pumped) "},{"citation":"resolved","description":"The 8756817 fix for the final panel's oracle low #4 stopped a zero price from zeroing the term, but it keeps `before` whole whatever the position's new principal. `_secured` bounds every term by SECURED_COLLATERAL_MULTIPLE x principal / price (line 851), and that per-position bound is the reason securedCollateral exists (finding 7cd5035c: 'a position with no debt contributes nothing and one wei of debt contributes two wei's worth'). The ungated `wipe` (line 514) calls `_reduceDebt` -> `_resecure(position, _priceOrZero())` (line 1215); while UsdPriceFeed._ethUsd or SharePriceFeed._rateOf reads zero (a reverting or malformed Chainlink answer, or sIMD's convertToAssets reverting: the exact cases the code handles by reading zero), the term written is min(before, collateral), not min(before, 2 x newPrincipal / lastPrice). A borrower who repays most of their principal during such an outage keeps a term sized for the old principal, and nothing re-prices it until that borrower is touched again (their own lock/free/draw/wipe, a redemption against them, or a bite); every other actor's checkpoint leaves it. `_securedCollateralValue` (line 757) then counts it up to the AGGREGATE cap prior x mat / 100, which is exactly where the per-position bound was doing the work: when other positions hold less than mat (a price fall, the state in which backing is below par and the redemption cap matters), the kept term fills the gap up to the cap and `_backingPerUnit` (line 707) reads par. `cash` (line 662) pays min(1, backing) x (1 - fee) of par from the Treasury's collateral first, so redeemers are overpaid from the reserve by the difference, and `backingPerUnit()` reports a backing the protocol does not have. The lagged figure does not help: `_clampLag` only lowers lagged values and the wipe lowers neither securedCollateral nor the lag. Reachable with the constants as committed by any borrower; the amplifier is the outage, which is rare (a stale Chainlink answer does not do this, only a reverting or malformed one), so low, like the finding whose fix this is. The prior zeroing erred toward under-counting; this errs toward over-counting, which is the unsafe direction for the redemption cap. NatSpec claim the code does not have: lines 842-845 'its collateral, bounded by the IMD that the multiple of its principal buys at that price' is no longer true of a term written while the leg is down. Smallest fix: with no price, shrink the term in proportion to the principal that remains rather than keeping it whole: `current = position.debt == 0 ? 0 : Math.min(before, Math.mulDiv(before, position.debt, debtBefore))` where debtBefore is the principal the term was last written for (pass it from _reduceDebt, or store the principal next to `secured` in the Position: one extra word written only on the dead-leg path), and keep `Math.min(_, position.collateral)`. A term that shrinks with the principal can never exceed 2 x principal / lastPrice, so the per-position bound survives the outage.","line":865,"path":"src/CDPVault.sol","reproduction":"test/scratch/DeadLegWipeKeepsTheTerm.t.sol (passes on this code: it demonstrates the state). Real ParameterizedVault over a MockIMD collateral priced $1 per 1e18 raw units (primary 5e14 wei per unit, an aggregator etched at CHAINLINK_ETH_USD answering 2000e8), reserve 100e18 units in the Treasury. B locks 1,000e18 and draws 500e18; A locks 2,000e18 and draws 500e18 (terms 1,000 each: min(C, 2 x 500 / 1)); a day passes. Price falls to $0.30: backingPerUnit == 0.63e18 ((100 + 2000) x 0.3 / 1000). vm.mockCallRevert on the aggregator's latestRoundData: collateralPriceFeed.isStale() is true, draws refuse; A calls wipe(490e18) and succeeds (ungated). securedCollateral is still 2,000e18: A's term stayed 1,000 for a principal of 10 (EXPECTED at most 2 x 10 / 0.3 = 66.7). Clear the mock, refresh the answer: backingPerUnit() == 1e18; EXPECTED (100 x 0.3 + (1000 + 66.7) x 0.3) / 510 = 0.686e18. B calls cash(25e18, 0, A): paid 25 x (1 - 2.96% fee) / 0.3 = 80.87 units of the reserve's collateral (asserted), against 55.5 at the honest backing: more than 1.4x, asserted. A then frees its surplus whenever it likes, re-pricing the term to 66.7 and leaving backing at 0.69 with the reserve 25 units lighter than an honest redemption would have left it.","severity":"low","snippet":"            ? (position.debt == 0 ? 0 : Math.min(before, position.collateral))","title":"CDPVault._resecure with a dead price leg keeps the position's whole secured term through a wipe, so principal repaid while Chainlink or the share vault reverts leaves a term for debt that no longer ex"},{"citation":"resolved","description":"The slot-3 repack (uint64 updatedAt, bool, uint40 anchorAt, uint24 bound, uint40 acceptedAt, uint80 epochFirst; verified with forge inspect: offsets 0, 8, 9, 14, 17, 22, exactly 32 bytes) changed `_anchorBound` from uint32 to uint24 (line 144, whose own comment is right: 'At most MAX_ALLOWANCE_BPS (1e6), so 24 bits are exact'), and `_accept` casts with uint24 (lines 475, 478). The constant's NatSpec and test/SwarmFeed.t.sol line 600 ('it is packed into a uint32') still name uint32. Documentation only; the cast is exact (1e6 < 2^24 = 16,777,216).","line":114,"path":"src/SwarmFeed.sol","reproduction":"Read src/SwarmFeed.sol:113-115 against line 144 and 478. EXPECTED per the NatSpec a uint32 field; ACTUAL `uint24 private _anchorBound` and `uint24(bound)`.","severity":"info","snippet":"    /// packed uint32 exact.","title":"SwarmFeed.MAX_ALLOWANCE_BPS NatSpec says it keeps 'the packed uint32 exact'; the field was repacked to uint24 in 8756817"},{"citation":"resolved","description":"`nearStale` (lines 301-306) measures age from `latestValue().updatedAt`, which SwarmFeed sets to the attestation's SIGNED issuedAt, and `submitAttestation` admits an issuedAt up to maxAge old. A relayer who holds an NHI attestation 18 hours and then relays it puts a value in that is near stale the moment it lands, so `ask(nhi)` pays at once. It is not an amplifier (the held attestation cost its buyer the same 0.5 IMD the Treasury then spends, and the value is genuinely 18 hours old), so the staleness trigger is still sound; only the sentence is wrong. The same held-relay asymmetry (freshness from the signature, silence from the relay) is the one 8756817 fixed in `_allowanceNow`, and this is its remaining harmless face.","line":38,"path":"src/OracleAsker.sol","reproduction":"OracleAsker fixture (test/OracleAsker.t.sol setUp), healthFeed seeded at T. Buy an NHI attestation with issuedAt = T + 1 hour and hold it; at T + 19 hours relay it through SwarmRelay (accepted: issuedAt >= _updatedAt, 18 hours old <= maxAge of 1 day). EXPECTED per the sentence the feed is 'fresh' for a day from the relay and cannot be asked for; ACTUAL nearStale(healthFeed) is true in the relay block (age 18h x 10,000 >= 24h x 7,500) and ask(healthFeed, body) pays 0.5 IMD from the Treasury's balance.","severity":"info","snippet":"///     nobody can make a feed age faster. Price feeds are NOT kept fresh on a clock; their one-hour","title":"OracleAsker NatSpec: 'nobody can make a feed age faster' is false for a held attestation, which lands already aged; nearStale and the keep-alive ask read the signed issuedAt"},{"citation":"resolved","description":"Treasury.fundOracle tops the asker up to ORACLE_BUDGET_PER_DAY (Treasury.sol lines 509-511), but the asker is a plain IERC20 balance holder with no cap: a direct transfer (runbook 7.4(b) tells the operator to send a day's budget right after the deploy; the keeper may send more) raises what `ask` can spend, and `ask` is permissionless and spends from balance with only the per-feed ASK_MIN_INTERVAL and in-flight limits. The bound that holds is the Treasury's own daily outflow, not the asker's holdings. No harm: extra IMD in the asker is only ever spent on attestations the chain shows a need for. The sentence should say the Treasury streams at most a day's budget, and whatever else the asker is given is spendable the same way.","line":55,"path":"src/OracleAsker.sol","reproduction":"OracleAsker fixture: imd.mint(address(asker), 100 ether) as in test/OracleAsker.t.sol setUp (the asker holds 100 IMD, 6.7 days of budget). EXPECTED per the sentence at most 15 IMD held; ACTUAL balance 100 IMD, and ask() pays from it with no reference to oracleBudget.","severity":"info","snippet":"/// daily budget, which is all this contract can ever hold. Exhausting the budget does NOT freeze the","title":"OracleAsker NatSpec: the daily budget is 'all this contract can ever hold', but anyone can transfer IMD to the asker and ask() spends whatever it holds; the runbook itself prefunds it"},{"citation":"resolved","description":"8756817 rewrote SwarmFeed.submitAttestation (lines 228-238) and the SwarmRelay header to say the relayer is not load-bearing on the shipped feeds: every shipped feed pins its question, and SwarmRelay forwards for anyone. The constant's NatSpec (lines 62-78) still carries the pre-epoch claim it cites ('Zero would mean permissionless relay, which SwarmFeed.submitAttestation documents as unsafe') and describes the address as 'deployed to Sepolia', while DeployMainnet._refuseUnlessReady requires it to equal the planned mainnet SwarmRelay. A reader of the constant alone concludes the relayer guards the first value, which the final panel's low #3 and the verifySeeded fix both say it does not. Documentation only.","line":63,"path":"src/DeploymentConfig.sol","reproduction":"Read src/DeploymentConfig.sol:62-64 against src/SwarmFeed.sol:233-236 ('The relayer is not a trust boundary on the shipped feeds: it is SwarmRelay, which forwards for anyone'). The two sentences contradict each other; the code matches the latter (SwarmRelay.relay has no caller check).","severity":"info","snippet":"/// which SwarmFeed.submitAttestation documents as unsafe for as long as questionHash binds a","title":"DeploymentConfig.ATTESTATION_RELAYER NatSpec still says a zero relayer is 'documented as unsafe' by SwarmFeed.submitAttestation and that the relayer is a Sepolia deployment; submitAttestation now says"}],"hash":"0060598dbda118de23e6bd356c5b4224d2322725e236ff8288ca4a7d46847184","nodeId":"fc75b6a8-17e9-4174-b2ee-9c694b2e0d97","outcome":"completed","summary":"The review is complete. Findings are in `.imd-findings.json` (3 low, 4 info, no medium or above), each anchored to a line whose snippet I verified against the tree. The project's feed, asker, relay and question-binding suites pass as committed (87/87). Three scratch tests under `test/scratch/` back the lows; one fails on this code as its finding predicts, two pass as demonstrations of a state.\n\n## Findings\n\n**Low. The new Treasury-paid test is spoofable by timing.** `onOracleResult` decides a refused answer was the Treasury's own by `lastAsk == inFlightAt`. The back-off branch writes `lastAsk` to a future time, now plus 6600 seconds, which is a multiple of 12 and therefore a real mainnet slot. A caller-paid `askPaid` mined in that slot gets `inFlightAt == lastAsk`, and when its hand-relayed answer makes the callback's relay revert, the Treasury is backed off another 1h50m. Chained, 0.5 IMD per cycle keeps `ask` reverting `TooSoon` indefinitely. The scratch test fails on the committed code with the pushed-out timestamp. Fix is a `treasuryPaid` flag in the `Feed` struct, which fits in the existing slot.\n\n**Low. The first-value check does not keep a raced value from pricing deposits.** The vault is open from its constructor, and `verifySeeded` runs off chain afterwards. It also compares against the live pool at the moment it runs, which is the state an attacker holds anyway. Both the stranger's 2x first value and a same-block draw of 4e18 land in the demonstration, and the honest value is refused for six hours. The imdUSD proceeds have no market on day one, which is the real brake.\n\n**Low. The dead-leg fix now over-counts.** A `wipe` while Chainlink or the share vault reverts keeps the position's whole secured term, including the part sized for principal just repaid. After recovery the kept term fills the aggregate mat cap. In the demonstration backing reads par where the honest figure is 0.69, and a redemption from the reserve pays 1.4x what it should.\n\n**Info.** Four NatSpec claims the code does not have: the \"packed uint32\" comment on `MAX_ALLOWANCE_BPS` (field is uint24), \"nobody can make a feed age faster\" (a held attestation lands already aged), the daily budget being \"all this contract can ever hold\" (the runbook prefunds the asker directly), and the relayer comment in `DeploymentConfig` that still cites the pre-epoch unsafe-relay text.\n\n## The six questions\n\n1. **Acceptance is sound.** Domain binds chain id and feed address, version 2. Replay is per feed via `usedRequests`. `issuedAt` is bounded above by now and `expiresAt`, below by `maxAge` and the last accepted time. Panel floors 25 and 15 are enforced. The question hash is rebuilt from the signed window with span, advance and 300-block recency bounds on mainnet. I found no path for a different question, chain, feed or window.\n\n2. **The bound holds as committed.** Packing verified with `forge inspect`: slot 3 offsets 0, 8, 9, 14, 17, 22, exactly 32 bytes. Every cast is exact. The silence clock runs from `_acceptedAt` after the first value, so the hold-then-relay walk is closed. Fastest sustained rate in either direction is the cap per lifetime (20% per hour on price and spot) because a stale base needs two hours of silence for 40%, which is slower. NHI is 20% per day on the fresh route, but its stale route is faster in absolute terms: 40% at 25 hours, then 2.5% per hour. Largest single step after H hours of silence is 4000 + 250(H-2) bps for the hour feeds and 4000 + 250(H-25) for NHI, capped at 1e6. A wide epoch is held to the cap around its first value, so nothing compounds faster than the cap per hour after the first step. Every gap is followed eventually; a 3x rise takes 66 hours, 100x about 166 days.\n\n3. **OracleAsker:** the keep-alive, wide-open and armed-drift triggers are correct, and the pool read uses the right slot and inversion. Nobody can exceed the daily budget or lose a delivered answer. The hold-off and the Treasury-paid test are the first finding.\n\n4. ","treeHash":null,"usage":{"cachedInputTokens":4858542,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":96053,"runtime":"claude","turns":53,"wallClockMs":1428931}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2b4e97bb63195f54","findings":[],"hash":"054923b0827fc3026905af95ab097d3759b68b1050bb3830b1a9b0baaa8a258b","nodeId":"2b1989a3-be18-44f3-8e19-341dd8c1d69c","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":55749}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"3f91b58cf7cd2d45","findings":[{"citation":"resolved","description":"Merged from four specialists (audit_permissions, audit_math, audit_economics, audit_flow); all four reproduce the same mechanism. The 8756817 fix for the final oracle panel's low #2 decides that the live request was the Treasury's own purchase by comparing two timestamps, on the reasoning at lines 266-268 that only `ask` writes lastAsk and inFlightAt together and no request can follow an `ask` within its second. But the catch branch at line 293 also writes lastAsk, to a FUTURE second: block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL = now + 6,600 s. 6,600 is a multiple of the 12-second slot, so on mainnet that instant is itself a block timestamp. In that block `ask` is still refused (TooSoon until lastAsk + 10 min) but `askPaid`/`askPaidMany` are not (lines 180-190: no interval check), and they write inFlightAt = block.timestamp = lastAsk. When the Intake later delivers that caller-paid request and the feed refuses it (the same signed attestation is public before the callback and SwarmRelay forwards for anyone, so a hand relay first makes the callback's relay revert ReplayedAttestation), `treasuryPaid` reads true and the catch writes lastAsk another 6,600 s out, landing exactly on the next slot boundary. Repeated, each cycle costs the caller the Intake's price (0.5 IMD) per two hours per feed and keeps Treasury-paid `ask` reverting TooSoon: the NHI keep-alive, an armed fall and a wide-open refresh are not bought with protocol money, which is the state the panel's low described (the keeper's own IMD remains the fallback, nothing is mispriced, so low as the panel rated it). The chain starts from any refusal of a Treasury purchase, which also needs no cause: hand-relaying the Treasury's own published answer before the Intake's callback makes that callback revert ReplayedAttestation and back the Treasury off two hours although its value landed. Reachable with the constants as committed (ASK_TIMEOUT 2 h, ASK_MIN_INTERVAL 10 min, 12 s slots; a missed slot costs one cycle). NatSpec claims the code does not have: lines 266-268 ('no request can follow an `ask` within its second') and 269-272 / 290-292 ('only the Treasury's own live purchase holds the Treasury back'). Smallest fix: record who paid instead of inferring it from timestamps: add `bool treasuryPaid` to `Feed` (the struct's second slot has spare bytes), set it in `ask` and clear it in `askPaid`/`askPaidMany` next to the inFlightAt write, and test `live && f.treasuryPaid` in the catch. Independently, do not back off on ReplayedAttestation / WindowNotAdvancing / StaleAttestation, which only say a value already landed or was overtaken.","line":273,"path":"src/OracleAsker.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\nimport {SwarmRelay} from \"src/SwarmRelay.sol\";\nimport {OracleAsker} from \"src/OracleAsker.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\nimport {\n    APPROVED_OPERATOR,\n    INTAKE,\n    ORACLE_ACTION,\n    ATTESTATION_RELAYER,\n    ASK_MIN_INTERVAL,\n    ASK_TIMEOUT\n} from \"src/DeploymentConfig.sol\";\n\n/// @dev A concrete SwarmFeed whose attester key the test holds (same shape as test/helpers/ConfigurableSwarmFeed).\ncontract ProofFeed is SwarmFeed {\n    constructor(address attester_, address relayer_, uint256 maxAge_, uint256 cap_)\n        SwarmFeed(attester_, relayer_, 1, 3, maxAge_, cap_)\n    {}\n\n    function seed(uint256 value) external {\n        _accept(value, uint64(block.timestamp));\n    }\n}\n\n/// @dev The Intake as PR #66 behaves: collects the price, records the callback, completes with a 200k stipend.\ncontract ProofIntake {\n    mapping(bytes32 => mapping(address => uint256)) public priceOf;\n    mapping(bytes32 => IIntake.Callback) public callbackOf;\n    uint256 public nonce;\n\n    function setPrice(bytes32 action, address asset, uint256 amount) external {\n        priceOf[action][asset] = amount;\n    }\n\n    function request(bytes32 action, bytes calldata, IIntake.Callback calldata callback, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 requestId)\n    {\n        uint256 price = priceOf[action][asset];\n        require(price != 0 && amount >= price, \"not sold\");\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        requestId = keccak256(abi.encode(block.chainid, address(this), ++nonce));\n        callbackOf[requestId] = callback;\n    }\n\n    function complete(bytes32 requestId, bytes calldata args) external returns (bool delivered) {\n        IIntake.Callback memory c = callbackOf[requestId];\n        (delivered,) = c.target.call{gas: 200_000}(bytes.concat(c.selector, args));\n    }\n}\n\n/// @notice OracleAsker.onOracleResult decides \"the Treasury paid\" by `f.lastAsk == f.inFlightAt`. A refused\n/// Treasury purchase writes lastAsk = now + ASK_TIMEOUT - ASK_MIN_INTERVAL, a FUTURE timestamp; an `askPaid`\n/// sent in the block with exactly that timestamp writes inFlightAt equal to it, so when the Intake delivers\n/// that caller-paid request and the feed refuses it (the caller hand-relayed the same bytes first) the\n/// callback treats it as the Treasury's own refusal and pushes lastAsk another two hours out. Repeating\n/// that every cycle holds the Treasury off indefinitely for 0.5 IMD per two hours: the state the final\n/// panel audit's low was meant to close.\ncontract BackOffForgeryTest is Test {\n    uint256 private constant ATTESTER_KEY = 0xA11CE;\n    uint256 private constant PRICE = 0.5 ether;\n    address private constant ATTACKER = address(0xA77);\n    bytes private constant HEALTH_BODY = '{\"question\":\"network health\"}';\n\n    MockIMD private imd;\n    ProofIntake private intake;\n    ProofFeed private healthFeed;\n    OracleAsker private asker;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(10 days);\n        vm.roll(1_000);\n        vm.etch(ATTESTATION_RELAYER, address(new SwarmRelay()).code);\n        vm.etch(INTAKE, address(new ProofIntake()).code);\n        intake = ProofIntake(INTAKE);\n        imd = new MockIMD();\n        intake.setPrice(ORACLE_ACTION, address(imd), PRICE);\n\n        healthFeed = new ProofFeed(vm.addr(ATTESTER_KEY), ATTESTATION_RELAYER, 1 days, 2000);\n        address[] memory feeds = new address[](1);\n        feeds[0] = address(healthFeed);\n        bytes32[] memory hashes = new bytes32[](1);\n        hashes[0] = keccak256(HEALTH_BODY);\n        bool[] memory tracks = new bool[](1);\n        bool[] memory keepAlive = new bool[](1);\n        keepAlive[0] = true;\n        asker = new OracleAsker(IERC20(address(imd)), feeds, hashes, tracks, keepAlive);\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(address(asker), 100 ether);\n        imd.mint(ATTACKER, 10 ether);\n        vm.stopPrank();\n        healthFeed.seed(0.9 ether);\n    }\n\n    function test_aCallerPaidRefusalTimedToTheBackOffReadsAsTreasuryPaidAndExtendsIt() public {\n        // The NHI keep-alive: 18h in, the Treasury buys. Its answer is refused (any refusal: here a bad\n        // signature; a hand relay of the same bytes before the callback does the same), so the back-off lands.\n        vm.warp(block.timestamp + 20 hours);\n        bytes32 r1 = asker.ask(address(healthFeed), HEALTH_BODY);\n        assertTrue(intake.complete(r1, abi.encode(r1, _attestation(r1, 0.9 ether), hex\"00\")), \"refused, callback completes\");\n        (,,, uint64 backedOffUntil,,,) = asker.feeds(address(healthFeed));\n        assertEq(backedOffUntil, uint64(block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL), \"the Treasury's own refusal backs it off\");\n\n        // ATTACKER waits for the block whose timestamp equals that future lastAsk and buys with its own IMD.\n        vm.warp(backedOffUntil);\n        vm.startPrank(ATTACKER);\n        imd.approve(address(asker), PRICE);\n        bytes32 r2 = asker.askPaid(address(healthFeed), HEALTH_BODY, PRICE);\n        vm.stopPrank();\n        (,,, uint64 lastAsk,, uint64 inFlightAt,) = asker.feeds(address(healthFeed));\n        assertEq(lastAsk, inFlightAt, \"a caller-paid request now carries the Treasury-paid signature\");\n\n        // The answer is public before the callback; ATTACKER relays it by hand, then the Intake delivers.\n        vm.warp(block.timestamp + 5 minutes);\n        SwarmFeed.OracleAttestation memory a = _attestation(r2, 0.9 ether);\n        bytes memory sig = _sign(a);\n        vm.prank(ATTACKER);\n        SwarmRelay(ATTESTATION_RELAYER).relay(healthFeed, a, sig);\n        assertTrue(intake.complete(r2, abi.encode(r2, a, sig)), \"refused as a replay, callback completes\");\n\n        // EXPECTED (the final panel audit fix, OracleAsker.sol:273-293): only the Treasury's own refused\n        // purchase backs the Treasury off, so a caller-paid refusal leaves lastAsk where it was.\n        (,,, uint64 after_,,,) = asker.feeds(address(healthFeed));\n        assertEq(after_, backedOffUntil, \"a caller-paid refusal must not extend the Treasury's back-off\");\n    }\n\n    function _attestation(bytes32 id, uint256 figure) private view returns (SwarmFeed.OracleAttestation memory a) {\n        a.requestId = id;\n        a.chainId = 1;\n        a.questionHash = keccak256(\"q\");\n        a.answerType = 3;\n        a.answer = abi.encode(figure);\n        a.figure = figure;\n        a.fromBlock = 100;\n        a.toBlock = 200;\n        a.blockHash = keccak256(\"b\");\n        a.panelJobId = keccak256(\"panel\");\n        a.panelSize = 60;\n        a.quorum = 20;\n        a.agreed = 40;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 1 hours);\n    }\n\n    function _sign(SwarmFeed.OracleAttestation memory a) private view returns (bytes memory) {\n        bytes32 body = keccak256(\n            bytes.concat(\n                abi.encode(\n                    healthFeed.ATTESTATION_TYPEHASH(), a.requestId, a.chainId, a.questionHash, a.answerType,\n                    keccak256(a.answer), a.figure\n                ),\n                abi.encode(\n                    a.fromBlock, a.toBlock, a.blockHash, a.panelJobId, a.panelSize, a.quorum, a.agreed,\n                    a.issuedAt, a.expiresAt\n                )\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) =\n            vm.sign(ATTESTER_KEY, keccak256(abi.encodePacked(\"\\x19\\x01\", healthFeed.DOMAIN_SEPARATOR(), body)));\n        return abi.encodePacked(r, s, v);\n    }\n}","reproduction":"test/scratch/Proof_fe155663af54.t.sol (the audit_economics proof, run by the judge: FAILS on this code with '949500 != 942600'). Fixture: SwarmRelay at ATTESTATION_RELAYER, a mock Intake at INTAKE selling oracle.request for 0.5 IMD, a 1-day/2000-bps keep-alive feed seeded at 0.9e18, asker holding 100 IMD, block.timestamp = 10 days (a multiple of 12). (1) T0+20h: ask(healthFeed, body) -> R1 (lastAsk = inFlightAt = T1). The Intake completes R1 with bytes the feed refuses: Delivered(relayed=false), feeds(healthFeed).lastAsk == T1 + 6600 (the Treasury's own back-off, expected). (2) vm.warp(T1 + 6600): ATTACKER approves 0.5 IMD and calls askPaid(healthFeed, body, 0.5e18) -> R2; feeds(healthFeed).inFlightAt == lastAsk (asserted). (3) +5 min: ATTACKER relays R2's attested answer through SwarmRelay by hand (accepted), then the Intake completes R2 with the same bytes: the callback's relay reverts ReplayedAttestation. EXPECTED (OracleAsker.sol:266-293): lastAsk unchanged at T1 + 6600 = 942600. ACTUAL: lastAsk == 949500 = (T1 + 6600 + 300) + 6600; the caller-paid refusal extended the back-off by two more hours, during which ask() reverts TooSoon. The test passes once the catch keys on an explicit Treasury-paid flag.","severity":"low","snippet":"        bool treasuryPaid = live && f.lastAsk == f.inFlightAt;","title":"OracleAsker.onOracleResult: the Treasury-paid test (lastAsk == inFlightAt) is satisfied by a caller-paid askPaid sent in the block whose timestamp equals a prior back-off's future lastAsk, so a caller"},{"citation":"resolved","description":"Merged from four specialists (audit_permissions low, audit_math medium, audit_economics info, audit_flow low). Q4. The 8756817 fix for the final oracle panel's low #3 (an unbounded first value relayed by anyone) is an operator-side view run after the broadcast. Two gaps remain against the scenario it was written for. (1) It is not a gate. ParameterizedVault is live from its constructor: lock, lockIMD and draw run as soon as the three feeds hold fresh values, and runbook 7.1 step 5 ('only then open deposits') is an announcement with no on-chain switch. run() deploys the feeds (layer 2) several transactions before the vault and never seeds; verify() REQUIRES every feed stale (line 298). The feed addresses are pure functions of the source and the Intake bodies name them, so attester-signed attestations for the planned addresses can be bought before the broadcast (a window must close within maxAge/12 blocks of the relay) and relayed in the block the feeds land. SwarmFeed._checkValue applies no bound while !_hasValue, so the first relayer anchors each feed, and in the same block can draw against it up to LINE at mat 170. (2) The comparison reference is `asker.poolPrice()` at the moment the script runs: the same pool the attested window sampled. Held at the attested level through the check, price, spot and the pool agree and the check prints 'Seeded and verified'. It also never reads the vault, so it reports success over a vault whose LINE is already drawn. Cost of a failed check: the honest value is refused ExcessDeviation until the allowance reaches the gap (a 2x anchor needs 5,000 bps: six hours of silence after the raced relay; a 3x anchor 6,667 bps, thirteen hours), and any draw made against the raced value stands: at a 2x anchor a position drawn to LINE sits at or under 100% once the market lands, so about $200k of each $1M drawn is unrecoverable at mat 170 by any bite. The runbook's rollback table allows abandoning the stack only before anyone deposits, so the practical remedy is a redeploy with new salts if the race is noticed before the announcement. Kept at low, as the panel rated the race: it needs attestations bought for an unknown deployment hour over a pool held through a 13-sample window, the imdUSD drawn has no market on day one, and verify()'s stale requirement catches a race that lands before run() returns. NatSpec claims the code does not have: SwarmFeed.sol:372-375 ('a raced first value is caught before deposits open') and 236-238, and this function's @notice (lines 341-349). Smallest fix: seed in the deployment itself so no block exists in which a feed is unseeded and the vault live (buy the three attestations for the planned addresses before the broadcast and relay them in run() right after layer 2, before the vault; replace verify()'s `require(f.isStale())` with verifySeeded's bands); and in verifySeeded also require `vault.totalDebt() == 0` and `gem.balanceOf(vault) == 0`, and compare against a reference the moment's pool does not set (the operator's reference price passed as an argument, or a pool TWAP over the hours before).","line":356,"path":"script/DeployMainnet.s.sol","reproduction":"test/scratch/FirstValueRace.t.sol (passes on this code: it demonstrates the state). ParameterizedVault over MockIMD with three SwarmFeed leaves (relayer SwarmRelay at ATTESTATION_RELAYER, data chain 1, cap 2000; price/spot 1 h, NHI 1 day), Chainlink etched at CHAINLINK_ETH_USD answering 2500e8, TreasuryFactory etched; all three feeds stale, as verify() requires. test_aRacedFirstValuePricesADrawBeforeAnyCheckCanRun: STRANGER relays attester-signed first values price 0.008 ETH (2x the 0.004 market), spot 0.008, NHI 0.9 through SwarmRelay: accepted, epoch anchor 2V, allowance 2000. Same block STRANGER lock(100_000e18) and draw(1_000_000e18): EXPECTED per the NatSpec no deposit is priced by a raced first value; ACTUAL draw succeeds, totalDebt == LINE, collateralRatio == 200. DEPLOYER's honest 0.004 attestation reverts ExcessDeviation now and at +5h59m59s; accepted at +6h; collateralRatio then reads 99. test_verifySeededPassesWhilePoolIsHeldAndADepositIsAlreadyPriced: after the same race and draw, `new DeployMainnet().verifySeeded(plan)` with the pool mock at the market reverts 'seeded: the price feed's first value is off the pool' (as designed, after the draw); with the pool mock at 2V it logs 'Seeded and verified' while vault.totalDebt() == LINE (asserted).","severity":"low","snippet":"        uint256 pool = asker.poolPrice();","title":"DeployMainnet.verifySeeded is advisory and compares against the live pool: the vault accepts lock and draw from its constructor, so a raced first value prices the racer's own deposit before the check "},{"citation":"resolved","description":"From audit_permissions; reproduced. Q5. The 8756817 fix for the final oracle panel's low #4 stopped a zero price from zeroing the term, but it keeps `before` whole whatever the position's new principal. `_secured` bounds every term by SECURED_COLLATERAL_MULTIPLE x principal / price (line 851), and that per-position bound is why securedCollateral exists (finding 7cd5035c). The ungated `wipe` (line 514) calls `_reduceDebt` -> `_resecure(position, _priceOrZero())` (line 1215); while UsdPriceFeed._ethUsd or SharePriceFeed._rateOf reads zero (a reverting or malformed Chainlink answer, or convertToAssets reverting: the cases the code handles by reading zero) the term written is min(before, collateral), not min(before, 2 x newPrincipal / lastPrice). A borrower who repays most of their principal during such an outage keeps a term sized for the old principal, and nothing re-prices it until that borrower is touched again (their own lock/free/draw/wipe, a redemption against them, or a bite); other actors' checkpoints leave it. `_securedCollateralValue` (line 757) counts it up to the aggregate cap prior x mat / 100, which is where the per-position bound was doing the work: when positions hold less than mat (a price fall, the state in which backing is below par and the redemption cap matters), the kept term fills the gap and `_backingPerUnit` (line 707) reads par. `cash` (line 662) then pays min(1, backing) x (1 - fee) of par from the Treasury's collateral first, so redeemers are overpaid from the reserve by the difference, and backingPerUnit() reports a backing the protocol does not have. The lag does not help: `_clampLag` only lowers lagged values and the wipe lowers neither securedCollateral nor the lag. Reachable with the constants as committed by any borrower; the amplifier is the outage (a stale Chainlink answer does not do this, only a reverting or malformed one), so low like the finding whose fix this is. The prior zeroing erred toward under-counting; this errs toward over-counting, the unsafe direction for the redemption cap. NatSpec claim the code does not have: lines 842-845 ('its collateral, bounded by the IMD that the multiple of its principal buys at that price') is not true of a term written while the leg is down. Smallest fix: with no price, shrink the term in proportion to the principal that remains rather than keeping it whole: pass the principal before the change into `_resecure` (or store it next to `secured`) and write `current = position.debt == 0 ? 0 : Math.min(Math.min(before, position.collateral), Math.mulDiv(before, position.debt, debtBefore))`. A term that shrinks with the principal never exceeds 2 x principal / lastPrice, so the per-position bound survives the outage.","line":865,"path":"src/CDPVault.sol","reproduction":"test/scratch/DeadLegWipeKeepsTerm.t.sol (passes on this code: it demonstrates the state). Real ParameterizedVault over MockIMD priced $1 per 1e18 raw units (primary 5e14 wei per unit, an aggregator etched at CHAINLINK_ETH_USD answering 2000e8 at block.timestamp), NHI 0.9 (mat 170), reserve 100e18 units in the Treasury. B locks 1,000e18 and draws 500e18; A locks 2,000e18 and draws 500e18 (securedCollateral == 2,000e18: each term min(C, 2 x 500 / 1)); a day passes. Price falls to $0.30: backingPerUnit() == 0.63e18 ((100 + 2,000) x 0.3 / 1,000). vm.mockCallRevert on the aggregator's latestRoundData: collateralPriceFeed.isStale() is true; A calls wipe(490e18) and succeeds (ungated). Clear the mock. securedCollateral is still 2,000e18 with A owing about 10 imdUSD (EXPECTED at most 2 x 10 / 0.3 = 66.7 for A's term). backingPerUnit() == 1e18 (EXPECTED about 0.686e18 = (30 + (1,000 + 66.7) x 0.3) / 510). B's cash(25e18, 0, A) against the reserve pays more than 1.4x what the same cash pays after A's term is re-priced by any touch (A lock(1): backingPerUnit() then reads 0.686e18 within 1%), asserted.","severity":"low","snippet":"            ? (position.debt == 0 ? 0 : Math.min(before, position.collateral))","title":"CDPVault._resecure with a dead price leg keeps the position's whole secured term through a wipe, so principal repaid during a Chainlink or share-vault outage leaves a term sized for debt that no longe"},{"citation":"resolved","description":"From audit_math; reproduced. Q5. UsdPriceFeed documents (lines 18-22) and SharePriceFeed repeats (lines 28-34) that a missing, paused or malformed aggregator answer reads as zero so that no consumer reverts; _ethUsd refuses non-positive answers, zero or over-uint64 timestamps and more than 77 decimal places for exactly that reason. It does not bound the answer's magnitude. latestRoundData's answer is an int256; for any positive answer with imdEth x answer >= 2^256 x 10^decimals, Math.mulDiv reverts (OpenZeppelin v5 MathOverflowedMulDiv) because the RESULT does not fit, and the revert propagates: UsdPriceFeed.latestValue, ethUsdPrice and SharePriceFeed.latestValue all revert while isStale() answers false (it reads the same answer as well formed). Consumers: ParameterizedVault._priceOrZero -> collateralPriceFeed.latestValue() is read on the ungated paths that promise never to revert (lock, lockIMD, wipe via _reduceDebt -> _resecure, cover's `last = _priceOrZero()`), so a borrower can neither repay nor add collateral while the leg answers that way; the Treasury's reserve valuation is unaffected (it uses _boundedCall, which reads a failed call as nothing). At the committed scale (imdEth about 4e15, 8 decimals) the threshold is an answer above about 2.9e69 against a real answer of about 2.5e11: not a market condition, but exactly the 'malformed' class the two feeds say they absorb, and the same class (over-uint80 round ids, over-77 decimals) that earlier fixes in this file bounded. Low. Smallest fix: in _ethUsd, refuse an answer that cannot be a price, e.g. `if (uint256(answer) > type(uint256).max / 1e36) return (0, 0, 0);` (far above any real price, far below the overflow point), so the documented zero is what every consumer sees.","line":43,"path":"src/UsdPriceFeed.sol","reproduction":"test/scratch/UsdLegOverflow.t.sol (passes on this code: it demonstrates the state). An aggregator etched at CHAINLINK_ETH_USD with 8 decimals and a settable answer; a never-stale IMD/ETH leg at 4e15; a ParameterizedVault over them with 1e18 of MockIMD locked. Answer 2500e8: usd.latestValue() == 4e15 x 2500e8 / 1e8 (correct). Answer 1e70 with a fresh timestamp: usd.isStale() == false; EXPECTED per the NatSpec latestValue() == (0, 0) and vault.lock(1e18) proceeds; ACTUAL usd.latestValue(), usd.ethUsdPrice() and vault.lock(1e18) all revert (MathOverflowedMulDiv).","severity":"low","snippet":"        value = Math.mulDiv(imdEth, ethUsd, 10 ** decimals);","title":"UsdPriceFeed.latestValue (and SharePriceFeed through it) reverts instead of reading zero when the ETH/USD answer is oversized, contradicting the 'anything unreadable reads as zero' contract that the v"},{"citation":"resolved","description":"Merged from four specialists. The slot-3 repack in 8756817 (forge inspect: _updatedAt uint64 @0, _hasValue bool @8, _anchorAt uint40 @9, _anchorBound uint24 @14, _acceptedAt uint40 @17, _epochFirst uint80 @22; exactly 32 bytes) narrowed `_anchorBound` from uint32 to uint24 (line 144, whose own comment is right: 'so 24 bits are exact'), and `_accept` casts with uint24 (lines 475, 478). The constant's NatSpec at lines 113-114 and test/SwarmFeed.t.sol:600 still name uint32. Line 21 ('Zero is rejected on both paths') predates the removal of the reporter fallback; there is one path. Every cast checked (Q2): uint24(maxDeviationBps) <= 10,000 and uint24(bound) <= 1,000,000 < 16,777,216; uint40(block.timestamp) to year 36812; uint80(value) only when value <= type(uint80).max; uint64(_anchorAt) in epoch(). Documentation only.","line":114,"path":"src/SwarmFeed.sol","reproduction":"Read src/SwarmFeed.sol:113-115 against line 144 (`uint24 private _anchorBound;`) and line 478 (`uint24(bound)`), and `forge inspect src/PriceFeed.sol:PriceFeed storage-layout`. EXPECTED per line 114 a uint32 field; ACTUAL uint24 at slot 3 offset 14, 3 bytes.","severity":"info","snippet":"    /// packed uint32 exact.","title":"SwarmFeed NatSpec: MAX_ALLOWANCE_BPS 'keeps the packed uint32 exact' (the field is uint24 since 8756817) and 'Zero is rejected on both paths' (there is one path); the packing and every cast are correc"},{"citation":"resolved","description":"From audit_economics; reproduced. Q2. The bound is exactly as `_checkValue`/`_epoch` state it: every value accepted within maxAge of an epoch's open lies within the allowance of the ANCHOR, and the next epoch anchors at whatever value was last accepted, with the cap because that value is fresh. Nothing holds the last value of one epoch and the first of the next apart in time, so a buyer who joins at the end of an honest epoch lands 1.2V in the epoch's last block and 1.44V in the next block, then 1.2 per hour. The figures the doc gives (2.07x at R+3h from the honest value at R) are right counted from the honest epoch's open; measured over any sliding hour the feed can move 44%, and from the buyer's first step 2.07x is two hours and one block. docs/PARAMETERS-2026-10-05.md 'a run of steps compounds at the cap per hour after the first' has the same imprecision (and 1.2^7 is 3.58, not 3.48). The precise statement is 'within one epoch, within the allowance of the anchor; consecutive epochs compound'. Documentation only; the sustained rate is unchanged.","line":30,"path":"src/SwarmFeed.sol","reproduction":"test/scratch/InfoDemos.t.sol test_twoStepsStraddleAnEpochBoundaryTwelveSecondsApart (passes on this code). StepFeed (SwarmFeed, maxAge 1h, cap 2000): seed V at R; at R+3588 accept 1.2V; at R+3600 epoch() reports anchor 1.2V with allowance 2000 and 1.44V is accepted. EXPECTED per line 30: refused as a second step inside one hour; ACTUAL accepted.","severity":"info","snippet":"/// now a feed anyone keeps alive moves at most the cap per hour however it is driven. An epoch opened","title":"SwarmFeed NatSpec 'moves at most the cap per hour however it is driven' is per epoch, not per sliding hour: two steps straddling an epoch boundary land 44% apart twelve seconds apart"},{"citation":"resolved","description":"Merged from audit_permissions and audit_flow; confirmed. `nearStale` (lines 301-306) measures age from `latestValue().updatedAt`, which SwarmFeed sets to the attestation's SIGNED issuedAt, and `submitAttestation` admits an issuedAt up to maxAge old (`_tooOld` is false at equality). A relayer who holds an NHI attestation 18 hours and then relays it puts in a value that is near stale the moment it lands, so `ask(nhi)` pays at once. It is one-for-one, not an amplifier: the held attestation cost its buyer the same 0.5 IMD the Treasury then spends, the value is genuinely 18 hours old, and it only works while no newer honest value has landed (StaleAttestation otherwise). The staleness trigger is still sound; only the sentence is wrong. The same held-relay asymmetry (freshness from the signature, silence from the relay) is the one 8756817 closed in `_allowanceNow`.","line":38,"path":"src/OracleAsker.sol","reproduction":"test/scratch/InfoDemos.t.sol test_aHeldAttestationLandsAlreadyAged (passes on this code). A 1-hour feed accepts a value whose issuedAt is exactly now - 1 hours; latestValue().updatedAt reads now - 1 hours (so nearStale's age x 10,000 >= maxAge x 7,500 at once), isStale() is false in the acceptance block and true one second later. EXPECTED per line 38 a relay cannot advance the feed's age; ACTUAL it arrives a full lifetime old.","severity":"info","snippet":"///     nobody can make a feed age faster. Price feeds are NOT kept fresh on a clock; their one-hour","title":"OracleAsker NatSpec 'nobody can make a feed age faster' is false for a held attestation, which lands already aged; nearStale reads the signed issuedAt"},{"citation":"resolved","description":"From audit_permissions; confirmed. Treasury.fundOracle tops the asker up to ORACLE_BUDGET_PER_DAY (Treasury.sol:509-511), but the asker is a plain IERC20 balance holder with no cap: a direct transfer (runbook 7.4(b) tells the operator to send a day's budget right after the deploy) raises what `ask` can spend, and `ask` is permissionless and spends from balance with only the per-feed ASK_MIN_INTERVAL, in-flight and price-ceiling limits. The bound that holds is the Treasury's own daily outflow, not the asker's holdings. No harm: extra IMD in the asker is only ever spent on attestations the chain shows a need for. The sentence should say the Treasury streams at most a day's budget, and whatever else the asker is given is spendable the same way.","line":55,"path":"src/OracleAsker.sol","reproduction":"test/OracleAsker.t.sol setUp mints 100 IMD to the asker (6.7 days of budget) and every ask() in that suite pays from it with no reference to oracleBudget. EXPECTED per line 55 at most 15 IMD held; ACTUAL any balance, and the shipped test fixture already holds 100.","severity":"info","snippet":"/// daily budget, which is all this contract can ever hold. Exhausting the budget does NOT freeze the","title":"OracleAsker NatSpec: the daily budget is 'all this contract can ever hold', but anyone can transfer IMD to the asker and ask() spends whatever it holds; the runbook itself prefunds it"},{"citation":"resolved","description":"Merged from three specialists; confirmed. 8756817 rewrote SwarmFeed.submitAttestation (lines 228-238) and the SwarmRelay header to say the relayer is not load-bearing on the shipped feeds: every shipped feed pins its question and SwarmRelay forwards for anyone. The constant's NatSpec (lines 62-78) still carries the pre-epoch claim it cites ('Zero would mean permissionless relay, which SwarmFeed.submitAttestation documents as unsafe') and describes the address as 'deployed to Sepolia', while DeployMainnet._refuseUnlessReady requires it to equal the planned mainnet SwarmRelay. A reader of the constant alone concludes the relayer guards the first value, which the final panel's low #3 and the verifySeeded fix both say it does not. Documentation only.","line":63,"path":"src/DeploymentConfig.sol","reproduction":"Read src/DeploymentConfig.sol:62-64 against src/SwarmFeed.sol:235-236 ('The relayer is not a trust boundary on the shipped feeds: it is SwarmRelay, which forwards for anyone'). The two sentences contradict each other; the code matches the latter (SwarmRelay.relay has no caller check).","severity":"info","snippet":"/// which SwarmFeed.submitAttestation documents as unsafe for as long as questionHash binds a","title":"DeploymentConfig.ATTESTATION_RELAYER NatSpec still says a zero relayer is 'documented as unsafe' by SwarmFeed.submitAttestation and that the relayer is a Sepolia deployment; submitAttestation now says"},{"citation":"resolved","description":"From audit_economics; recomputed by the judge. keccak256(QUESTION_PREFIX || '26120928' || ',\"toBlock\":' || '26121526' || '}}') == 0xc87aa8a9fa49ca4885e3c3048e9159cd00578e7ad37188b1fce70199c510c16e, the questionHash in oracle/attestation-e2c85027.json (window 26120928..26121526, span 598, figure 3729511079526129, domain version 2). So PriceFeed's pinned prefix is verified against a live signature and the sentence at lines 36-38 is stale in the pessimistic direction. SpotFeed.sol:43 and NhiFeed.sol:36 still say 'Nothing has been bought with it yet' while docs/LAUNCH-READINESS.md says live NHI and SPOT attestations were proven on testnet; their request ids are not archived in this repository. Documentation and launch-record item: archive those two request ids (or re-verify with --verify before the freeze) and reword the three passages.","line":38,"path":"src/PriceFeed.sol","reproduction":"python3: prefix = bytes.fromhex(PriceFeed.QUESTION_PREFIX); doc = prefix + b'26120928,\"toBlock\":26121526}}'; `cast keccak` of doc prints 0xc87aa8a9...c510c16e, equal to message.questionHash in oracle/attestation-e2c85027.json. EXPECTED per the NatSpec: no live attestation checks the constant; ACTUAL: one in the repository does and matches.","severity":"info","snippet":"    /// yet, so this constant has NOT been checked against a live attestation.","title":"PriceFeed NatSpec says the pinned prefix 'has NOT been checked against a live attestation'; it reproduces the questionHash signed in the archived live attestation oracle/attestation-e2c85027.json byte"},{"citation":"resolved","description":"Not a defect: the answers the task asks for where nothing is wrong, checked against the code and the shipped tests. Q1: the EIP-712 domain binds block.chainid and address(this), so an attestation signed for PriceFeed is refused by SpotFeed and by any other chain; usedRequests is per feed; issuedAt must be <= now, <= expiresAt, not older than maxAge and not older than the last accepted value; panelSize >= 25 and 15 <= agreed <= panelSize; the question hash is recomputed from the pinned prefix and the SIGNED fromBlock/toBlock, the span is bounded (300..1200 price, 150..1200 spot/NHI), toBlock must advance past lastToBlock, and on chain 1 (every mainnet feed) must be <= block.number and at most maxAge/12 blocks old; s is low and v is 27/28. No attestation for a different question, chain, feed or window is accepted. Q2: an epoch opens at the relay block and lasts maxAge; every value in it is within the stored bound of the anchor; the next epoch anchors at the last accepted value. Sustained rate from a fresh feed: (1 + cap) per maxAge in either direction, 1.2 per hour for price/spot (2.07x at 3 h, 3.58x at 6 h; 0.8/h down), 1.2 per DAY for NHI; two steps can straddle an epoch boundary (info above). Silence from the LATER of signature and relay (this line) earns the stale base only after a whole hour past the lifetime, so the largest single step after H hours of silence is 20% for H < 2, else 4,000 + 250 x (H - 2) bps (40% at 2 h, 50% at 6 h, 60% at 10 h, 100% at 26 h, the 1e6 cap at about 3,986 h); NHI the same with H - 25. A stale opening is slower than the cap per hour (1.4 per 2 h < 1.44), every acceptance resets the silence, holding an attestation back cannot help (test_aHeldAttestationDoesNotEarnTheStaleBase passes), and a wide epoch holds later values to the cap around its first. Every gap is followed as a delay. Packing and casts: see the info above. Q3: ask (keep-alive near stale, wideOpen = stale and no live epoch and allowance >= 6,000, armed fall), askPaid, askPaidMany, _request (feedOf survives a timeout) and onOracleResult behave as documented except the back-off gap (low above). Nobody can exceed the Treasury's daily outflow; a held attestation can advance the keep-alive one-for-one (info); no paid answer is lost (feedOf persists; a refused answer stays public). Q4: reported (low above). Q5: units are right throughout (imdEth x ethUsd / 10^dec = USD per 1e18 raw IMD; rate x assetUsd / 1e18 = USD per 1e18 raw sIMD, which _collateralRatio handles through its remainder path since the price is far below 1e16); a reverting, short or malformed Chainlink or share-vault answer reads as (0,0) and stale: gated paths revert StaleFeed, lock/lockIMD/wipe keep the secured term (with the over-counting gap above), cover with a zero last price requires fresh feeds, _clearIfRecovered preserves the mark, the Treasury counts the asset for nothing; the one exception is the oversized-answer revert (low above). Q6, at LINE $1M, mat 170, cap 2000, pool $2.3M a side at 1%: up, seven rungs over six hours from a fresh feed (six over five from a two-hour-silent one), each rung the 13-sample median held for 7 samples (about an hour) plus a single pushed block for spot; pool fees about $40k cumulative plus unbounded exposure to holders selling into it; prize: draw $1M against collateral worth $1M x 1.7 / 3.58 = $475k at market, then a self-liquidation after the correction leaves about $500k of imdUSD unbacked, which has no market on day one. Down: 0.8/h; two rungs (0.64) put every position at mat under water; the bonus is 20% of debt repaid, at most $200k on the whole LINE, less the loss on reselling seized sIMD into the dumped pool, and it needs the dump held through the six-hour grace. The hold-then-relay variant earns nothing since 8756817 (silence from the relay). What stops the walk is cost and visibility, not a gate: the Treasury never pays for a rise, and no honest value can land once the walk is two rungs ahead. Coverage: read in full Swar","line":436,"path":"src/SwarmFeed.sol","reproduction":"Not a defect; the arithmetic above is checked against SwarmFeed._allowanceNow (lines 428-443), _accept (466-490), _checkValue (388-404), _requireQuestion (315-329) and the regression tests test_relayingAnHourApartNeverEarnsTheStaleBase and test_aHeldAttestationDoesNotEarnTheStaleBase, which pass on this tree.","severity":"info","snippet":"        uint256 since = _acceptedAt > _updatedAt ? _acceptedAt : _updatedAt;","title":"Numbered answers and coverage: acceptance sound; the per-epoch bound holds at the cap per epoch (1.2/h price feeds, 1.2/day NHI) with the first step after H hours of silence 40% + 2.5%(H-2) (NHI: H-25"}],"hash":"1db7293d2481b80a187762d1d5323701628e86b6e65bb71a6ad0463aa639f5a8","nodeId":"2b1989a3-be18-44f3-8e19-341dd8c1d69c","outcome":"completed","summary":"The review is complete. The findings file holds 11 entries, all reproduced against the code, with the one attached proof confirmed to fail on this tree for its stated reason. Nothing in the tracked tree changed; only the findings file and test/scratch/ were written.\n\n## Judgement\n\n**Kept (4 low, 7 info), merged from 23 specialist reports:**\n\n- **Low, OracleAsker back-off forgery** (merged from all four specialists). The catch writes lastAsk to now + 6,600 s, a mainnet slot boundary, so a caller-paid askPaid mined in that second satisfies the Treasury-paid test and its refusal extends the back-off. The attached proof fails here with `949500 != 942600`. Fix: an explicit treasuryPaid flag in the Feed struct.\n- **Low, verifySeeded is advisory** (merged from four; I kept the panel's low rather than the math specialist's medium). The vault is live from its constructor, the check compares against the live pool and never reads the vault. My test shows the LINE drawn against a raced 2x first value, and verifySeeded printing \"Seeded and verified\" over that vault while the pool mock sits at 2x.\n- **Low, dead-leg wipe keeps the whole term.** The 8756817 fix keeps min(before, collateral) with no price, so a borrower repaying 490 of 500 during an outage keeps a term for 500. Reproduced: backingPerUnit reads par where the honest figure is 0.686, and a redemption against the reserve pays over 1.4x what it should.\n- **Low, oversized ETH/USD answer reverts** instead of reading zero, reaching the ungated lock and wipe. Reproduced with an answer of 1e70.\n- **Info:** uint32/uint24 NatSpec; \"cap per hour\" is per epoch (44% across a boundary, demonstrated); \"nobody can make a feed age faster\" (held attestation lands aged, demonstrated); asker budget sentence; DeploymentConfig relayer NatSpec; PriceFeed prefix actually reproduces the archived live attestation's questionHash; and the numbered answers below.\n\n**Dropped:** nothing failed to reproduce. The specialists' \"$169k collateral at 3.48x\" figure was wrong (debt times mat over k gives $475k at 1.2^7); corrected in the Q6 answer.\n\n## Numbered answers, briefly\n\n1. Acceptance is sound: domain binds chain and feed, replay per feed, issuedAt bounded both ways, panel floors enforced, question hash spliced from the signed window with span, advance and recency bounds. No cross-question, chain, feed or window acceptance.\n2. The bound holds per epoch: 1.2 per hour on price feeds, 1.2 per day on NHI, first step after H hours of silence 40% + 2.5%(H−2) (NHI: H−25), 100% at 26 hours. Packing is exactly 32 bytes and every cast is exact. The hold-then-relay variant is closed. No sequence beats the cap per epoch after the first step.\n3. Ask paths behave as documented except the back-off gap. No budget overrun, no lost paid answer.\n4. The check is not sufficient for the racer's own deposit, and a failed check costs a redeploy or the unrecoverable share of any draw made first.\n5. Units are right; dead legs read zero and stale, with the two gaps reported.\n6. The walk costs about $40k of pool fees over six hours for roughly $500k of unbacked imdUSD at the LINE; downward, at most the $200k bonus less resale loss, needing the dump held through the six-hour grace.\n\n**Coverage:** read in full SwarmFeed, OracleAsker, SwarmRelay, the three leaves, UsdPriceFeed, SharePriceFeed, DeploymentConfig and DeployMainnet. CDPVault and ParameterizedVault were read on every price-consuming path. Not read in full: Treasury, Parameters, SwarmWorkOracle, and the bite/bark internals.","treeHash":null,"usage":{"cachedInputTokens":3208067,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":56100,"runtime":"claude","turns":50,"wallClockMs":809626}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ee9fbaf2480d1034","findings":[{"citation":"resolved","description":"The fix in 8756817 for the final panel's oracle low decides that the live request was the Treasury's purchase when `f.lastAsk == f.inFlightAt`, reasoning that only `ask` writes both in one call. But the catch branch (line 293) writes lastAsk to a FUTURE second, `block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL` (now + 6600 s = exactly 550 twelve-second slots), and `askPaid`/`askPaidMany` write `inFlightAt = block.timestamp` with no TooSoon check. An `askPaid` mined in exactly that second therefore produces `inFlightAt == lastAsk`, and when its answer is refused (the attacker hand-relays the published attestation first, so the callback hits ReplayedAttestation) the catch treats the caller-paid request as the Treasury's and writes lastAsk another 6600 s ahead. Repeating every ~2 h (0.5 IMD each, about $60/day per feed) holds Treasury-paid asks off indefinitely: no armed fall is bought, the NHI keep-alive at 18 h is not bought, and a wide-open silent feed is not refreshed by the Treasury (the doc's defence against a single-shot re-anchor, PARAMETERS 'And the refresh that closes the silence'). The chain is started either by any genuine Treasury refusal or, for free, by hand-relaying the Treasury's OWN published answer before the Intake's callback: `live && lastAsk == inFlightAt` is true for that request, the relay reverts ReplayedAttestation, and the Treasury is backed off two hours although its answer landed and the feed is healthy. The NatSpec at lines 266-272 and 288-292 ('only the Treasury's own live purchase holds the Treasury back'; 'a caller-paid answer ... says nothing about the feed') claims a property the code does not have. Reachable with the constants as committed (ASK_TIMEOUT 2 h, ASK_MIN_INTERVAL 10 min, SwarmRelay permissionless). Manual fallbacks (keeper askPaid, hand relay) remain and nothing is mispriced, so low. Smallest fix: record who paid explicitly (a `bool treasuryPaid` in the Feed struct set by `ask`, cleared by `askPaid`/`askPaidMany`) and, independently, do not back off when the relay failed with ReplayedAttestation or WindowNotAdvancing, which only say a value already landed; or write the back-off as `lastAsk = block.timestamp` plus a separate `backedOffUntil` so lastAsk is never a future second an askPaid can match.","line":273,"path":"src/OracleAsker.sol","reproduction":"Fixture as test/OracleAsker.t.sol (MockIntake at INTAKE, SwarmRelay at ATTESTATION_RELAYER, pool mock at POOL_MANAGER, 1-hour price feed seeded at 0.001 ETH, asker holding 15 IMD, tracksPool). (1) Pool falls 10%; arm; 5 blocks later ask() -> R1 (Treasury pays). The attacker relays R1's published attestation by hand through SwarmRelay (gas only), then the Intake completes R1: ReplayedAttestation, lastAsk = T1 + 6600. (2) At block.timestamp == T1 + 6600 exactly (550 slots later), attacker calls askPaid(priceFeed, body, 0.5e18) -> R2: feeds(priceFeed).inFlightAt == lastAsk. (3) Attacker hand-relays R2's attestation, Intake completes R2. EXPECTED (per lines 266-272): lastAsk unchanged at T1 + 6600, so the Treasury may ask again at T1 + 7200. ACTUAL: lastAsk = T1 + 6600 + 6600 = T1 + 13200 (test/scratch/ForgedTreasuryPaid.t.sol fails with '877200 != 870600'), and ask() reverts TooSoon for two more hours. Repeat at each new lastAsk for 0.5 IMD per cycle.","severity":"low","snippet":"        bool treasuryPaid = live && f.lastAsk == f.inFlightAt;","title":"OracleAsker.onOracleResult: the Treasury-paid test (lastAsk == inFlightAt) is forgeable, so a caller-paid refusal still backs the Treasury off for two hours; and anyone can trigger the back-off for fr"},{"citation":"resolved","description":"The fix in 8756817 for the final panel's oracle low (an unbounded, permissionlessly relayed first value) is a script-side check run by the operator after they seed. Nothing on chain gates deposits: ParameterizedVault is live from its constructor (no pause, no 'deposits open' state), SwarmRelay forwards for anyone, the planned CREATE2 feed addresses and the Intake bodies are in the repository before the broadcast, and `run()` deploys the three feeds several transactions before the vault and never seeds. A racer who holds attester-signed attestations for the planned addresses (bought at any time in the hour before, with a window closing within the last 300 blocks, over a pool pumped to 2x for 7 of the 13 samples: about $19k of pool fees round trip at $2.3M a side) relays price, spot and NHI in the block after the vault lands and, in the same block, locks sIMD and draws. The feed's first epoch is then anchored at the racer's value and the deployer's honest value is refused ExcessDeviation until `_allowanceNow` reaches 5,000 bps, six hours later (22 hours for a 10x anchor, 26 for anything beyond). `verifySeeded` therefore detects the race for the operator but keeps nobody out, and the NatSpec here, at SwarmFeed.sol:236-238 ('verifySeeded checks it against the pool before deposits open') and at SwarmFeed.sol:372-375 ('a raced first value is caught before deposits open') claims a gate the code does not have. What a failed check costs: the racer has already borrowed up to LINE against collateral worth about half of it at market; when the honest value is finally accepted the position sits at CR 100 and bites can recover at most the collateral, leaving about $200k of debt uncovered per $1M drawn (the racer's profit is paper until imdUSD has a market, which is why this stays low); the runbook's rollback table says 'after the first deposit there is no rollback, only migration', so the practical cost is abandoning the stack (new salts, a second broadcast of up to 0.05 ETH, 1.5 IMD of refused purchases) or living with the bad debt. Reachable with the constants as committed. Smallest fix: seed in the deployment itself so no block exists in which a feed is unseeded and the vault live: buy the three attestations for the planned addresses before the broadcast and relay them through SwarmRelay in `run()` in the same transaction as (or before) the vault's CREATE2; or have the vault refuse `draw` while `priceFeed.lastToBlock()` is below a deployment-recorded block. Reword the two SwarmFeed passages and this NatSpec to say the check is operator-side.","line":341,"path":"script/DeployMainnet.s.sol","reproduction":"test/scratch/FirstValueRaceVault.t.sol (passes on this code: it demonstrates the state). Chain id 1, SwarmRelay at ATTESTATION_RELAYER, TreasuryFactory at TREASURY_FACTORY, Chainlink mock at CHAINLINK_ETH_USD answering $2,000, sIMD mock over IMD at 7.95 IMD/sIMD, three SwarmFeed leaves (1 h, 1 h, 1 day; cap 2000) and a ParameterizedVault over them, all unseeded. STRANGER relays via SwarmRelay: price 0.002 ETH (2x the 0.001 market), spot 0.002 ETH, NHI 0.9. Same block: lockIMD(500,000 IMD, worth $1,000,000) and draw(1,000,000 imdUSD). EXPECTED per the NatSpec ('caught before deposits open'): no deposit can be priced off the raced value. ACTUAL: draw succeeds (honest capacity at mat 170 is 588,235), collateralRatio reads 200. DEPLOYER's honest 0.001 ETH attestation reverts ExcessDeviation. Six hours later epoch() allowance is 5,000 and the honest value lands; collateralRatio then reads 100 with 1,000,000 imdUSD outstanding.","severity":"low","snippet":"    /// @notice Runbook section 7.1, after the first attestations are relayed and BEFORE deposits open:","title":"DeployMainnet.verifySeeded is advisory: the vault accepts lock and draw from its constructor, so a raced first value prices the racer's own deposit (the whole LINE) before the operator can run the che"},{"citation":"resolved","description":"8756817 repacked slot 3 to (uint64 updatedAt, bool, uint40 anchorAt, uint24 anchorBound, uint40 acceptedAt, uint80 epochFirst) and updated the field comment at line 143 ('so 24 bits are exact') and line 470, but the constant's own NatSpec still names a uint32. The packing itself is correct: 64+8+40+24+40+80 = 256 bits, one slot; 1e6 < 2^24 so `uint24(bound)` never truncates; uint40 timestamps last to year 36812; `uint80(value)` is guarded by `value <= type(uint80).max`. Documentation only; test/SwarmFeed.t.sol:600 carries the same stale 'uint32'. Fix: 'keeps the packed uint24 exact'.","line":114,"path":"src/SwarmFeed.sol","reproduction":"Read src/SwarmFeed.sol:113-115 against line 144 (`uint24 private _anchorBound;`). EXPECTED: the comment names the field's type. ACTUAL: it names uint32, the type before 8756817.","severity":"info","snippet":"    /// packed uint32 exact.","title":"SwarmFeed.MAX_ALLOWANCE_BPS NatSpec says it keeps 'the packed uint32 exact' after the repack made the field a uint24"},{"citation":"resolved","description":"`nearStale` measures age from the value's signed issuedAt (SwarmFeed.latestValue), and `submitAttestation` accepts an issuedAt up to maxAge old. A relayer who holds an NHI attestation bought at t0 and relays it 18 hours later (toBlock within maxAge/12 blocks of head, issuedAt >= the current value's) lands a value that is immediately 75% of the way to maxAge, so `ask(nhi)` pays 0.5 IMD of Treasury money in the same block. The attacker paid 0.5 IMD for the held attestation and the Treasury's purchase delivers an honest fresh value, so this is a one-for-one grief bounded by the daily budget, not an amplification, and it only works while no honest value newer than t0 has landed (otherwise StaleAttestation). Documentation only: the staleness ask does not need arming because the grief is unprofitable, not because ageing cannot be forced. Fix: reword to 'a relayed value can arrive already old, but only at the relayer's own cost'.","line":38,"path":"src/OracleAsker.sol","reproduction":"OracleAsker.t.sol fixture: healthFeed (1-day lifetime, keepAlive) holds a value with issuedAt T0. At T0+10h the Treasury would not pay (nearStale false until T0+18h). A buyer relays, through SwarmRelay at T0+10h, an attestation signed at T0+1m with figure inside the cap (accepted: issuedAt >= T0 and not older than maxAge). Observe nearStale(healthFeed): still false (age 9h59m). Now at T0+18h+1 the same attack with an attestation signed at T0+1m: accepted, and `ask(healthFeed, body)` pays 0.5 IMD of Treasury money in the same block, delivering a value the Treasury would have bought at T0+18h anyway. The simplest statement of the claim that fails: seed a 1-hour feed with an attestation whose issuedAt is exactly now - 1 hours (accepted: `_tooOld` is false at equality). EXPECTED per line 38: a relay cannot advance the feed's age. ACTUAL: latestValue().updatedAt reads now - 1 hours, nearStale() is true in the acceptance block and isStale() one second later.","severity":"info","snippet":"///     nobody can make a feed age faster. Price feeds are NOT kept fresh on a clock; their one-hour","title":"OracleAsker NatSpec: 'nobody can make a feed age faster' is false; relaying a held attestation makes nearStale() true at once, so anyone can advance the Treasury's keep-alive purchase (one-for-one, no"},{"citation":"resolved","description":"After 8756817 SwarmFeed.submitAttestation's NatSpec (lines 235-236) states 'The relayer is not a trust boundary on the shipped feeds: it is SwarmRelay, which forwards for anyone', and SwarmFeed's constructor comment (193-205) says the nonzero-relayer check does not restrict callers. DeploymentConfig lines 62-64 still cite the pre-epoch reasoning that submitAttestation 'documents as unsafe' a permissionless relay, and lines 66-78 describe the Sepolia deployment. The code is consistent (every shipped feed pins its question, so the relayer carries no trust); the comment a reviewer of the mainnet constants reads describes the opposite model. Fix: state that ATTESTATION_RELAYER is a permissionless forwarder pinned for bundling, that the question binding is the guard, and that the first value is bounded by nothing on chain.","line":62,"path":"src/DeploymentConfig.sol","reproduction":"Read src/DeploymentConfig.sol:62-64 against src/SwarmFeed.sol:235-238. EXPECTED: one description of what the relayer guards. ACTUAL: DeploymentConfig says permissionless relay is documented as unsafe; SwarmFeed says the relayer is not a trust boundary and SwarmRelay admits everyone.","severity":"info","snippet":"/// @dev Sole address permitted to submit an attestation. Zero would mean permissionless relay,","title":"DeploymentConfig ATTESTATION_RELAYER NatSpec still says a zero relayer 'would mean permissionless relay, which SwarmFeed.submitAttestation documents as unsafe'; SwarmFeed now documents the relayer as "}],"hash":"625a6c7abd63f664ab5799b0287306cbe5c8f457c4e971afc5b2626665578fc3","nodeId":"959999cc-5a02-4bdd-8961-ad519eea41be","outcome":"completed","summary":"All five findings are anchored and verified; the two low findings each have a scratch test that reproduces the state on the committed code. Here is the review.\n\n## Outcome\n\nThe newest fixes in 8756817 hold where they matter most: the per-epoch bound with silence measured from the relay is correct, and I found no sequence that beats the cap per hour. Two of the fix's companion claims do not hold. The Treasury-paid test in the asker callback is forgeable, and the first-value check is operator-side only, so a raced first value still prices the racer's own deposit. Findings are in `.imd-findings.json`: 2 low, 3 info, no critical or high, so no proof files were attached.\n\nShipped oracle-path suites pass on the committed tree (105 tests across SwarmFeed, OracleAsker, QuestionBinding, RelayBundling, SwarmRelay, UsdDenomination, ShareCollateral).\n\n## Findings\n\n- **Low, OracleAsker.onOracleResult line 273.** The Treasury-paid test `lastAsk == inFlightAt` is forgeable. The catch writes lastAsk to a future second, exactly 550 slots ahead. An `askPaid` mined in that second produces equal fields, and a hand-relayed answer makes the callback refuse, so the caller-paid refusal backs the Treasury off two more hours for 0.5 IMD. The chain can also be started for free by hand-relaying the Treasury's own published answer before the Intake's callback. `test/scratch/ForgedTreasuryPaid.t.sol` fails on this code with `877200 != 870600`. Fix: an explicit `treasuryPaid` flag, and no back-off on ReplayedAttestation or WindowNotAdvancing.\n- **Low, DeployMainnet.verifySeeded line 341.** The vault is live from its constructor and the planned feed addresses are public before the broadcast. A racer relays 2x values into the unseeded feeds and, in the same block, locks sIMD worth $1M and draws the whole $1M LINE. The deployer's honest value is refused for six hours. `test/scratch/FirstValueRaceVault.t.sol` demonstrates it. Fix: seed in the deploy transaction, or gate the first draw on chain.\n- **Info ×3.** SwarmFeed line 114 still says \"packed uint32\" after the uint24 repack. OracleAsker line 38 says \"nobody can make a feed age faster\", but a held attestation lands already old. DeploymentConfig line 62 still describes permissionless relay as documented unsafe, contradicting SwarmFeed lines 235 to 238.\n\n## Numbered questions\n\n1. **Attestation acceptance is sound.** Domain binds chain and feed address with version 2; requestId replay is per feed; issuedAt is bounded both ways and must not precede the last value; panel floors 25 and 15 are enforced; the question hash is spliced from the signed window with span, advance and recency bounds, and the recency bound applies on mainnet since the data chain is this chain. Signature malleability is refused. I found no cross-question, cross-chain, cross-feed or stale-window acceptance.\n2. **The per-epoch bound is correct as committed.** Packing is 64+8+40+24+40+80 bits, one slot, every cast exact. Within an epoch every value is within the anchor's allowance, and the next epoch opens at the earliest one lifetime later, anchored at the last value. The stale base needs the current time to be at least two hours past the later of signature and relay, so holding an attestation buys nothing. Fastest rate in either direction: the cap at once, then the cap per hour, so 1.2^(k+1) at k hours for price feeds, 1.2 per day for NHI. Largest single step after H hours of silence is 20% below two hours, then 40% plus 2.5% per further whole hour, capped at 100x; NHI runs the same schedule from 25 hours. Every fall is followed within 26 hours for price feeds and 49 for NHI. Only a rise above 101x is never followed.\n3. **OracleAsker.** ask, askPaid, askPaidMany, the timed-out request keeping feedOf, and wideOpen behave as documented. Nobody can exceed the daily budget or lose a paid answer. The Treasury-paid test is fakeable, and hold-off is possible at 0.5 IMD per two hours per feed, as in the first finding.\n4. **The first value.** verifySe","treeHash":null,"usage":{"cachedInputTokens":4770388,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":78051,"runtime":"claude","turns":53,"wallClockMs":1204090}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d11ea2b5e05fa7a8","findings":[{"citation":"resolved","description":"The fix in 8756817 decides whether the live request was the Treasury's own by comparing two timestamps: `ask` writes lastAsk and inFlightAt in the same call, `askPaid` writes only inFlightAt. The comment at lines 266-268 argues no request can carry that signature unless `ask` made it. But the catch at line 293 writes lastAsk = block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL, a timestamp 6,600 s in the FUTURE, and 6,600 is a multiple of the 12 s slot, so on mainnet that value is exactly the timestamp of a block that will exist. An attacker who sends `askPaid` (or `askPaidMany`) in that block writes inFlightAt == lastAsk; when the Intake delivers that caller-paid request and the feed refuses it (the attacker hand-relays the same public bytes through SwarmRelay first, so the callback's relay reverts ReplayedAttestation), `treasuryPaid` reads true and the catch pushes lastAsk another 6,600 s out. Repeating this every cycle holds Treasury-paid `ask` off for as long as the attacker likes, at the Intake's price (0.5 IMD, ~$5) per two hours per feed, which is exactly the state the final panel audit's low (OracleAsker.onOracleResult, 'a ~$5 askPaid ... disables Treasury-paid asks') was meant to close. The first back-off in the chain is the Treasury's own refused purchase, which the attacker can also force for free: front-run the Intake's callback for a Treasury `ask` with a hand relay of the same attestation. Effect: the NHI keep-alive (the only clock-driven purchase) and armed-fall refreshes are not bought by the Treasury; the keeper must pay with its own IMD or NHI goes stale after a day and every price action halts. Nothing is mispriced, so low, as the panel rated it. The NatSpec claim the code does not have: lines 266-268 ('no request can follow an `ask` within its second (it is in flight)') and 269-272 ('Only a refusal of the Treasury's own purchase backs the Treasury off'). Reachable with the constants as committed (ASK_TIMEOUT 2h, ASK_MIN_INTERVAL 10 min, 12 s slots; a missed slot just costs the attacker one cycle). Smallest fix: record who paid explicitly instead of inferring it from timestamps: add `bool treasuryPaid` to `Feed` (the second slot has room), set it in `ask` before `_request` and clear it in `askPaid`/`askPaidMany`, and test that flag in the catch; or, equivalently, do not treat a FUTURE lastAsk as a match (`f.lastAsk == f.inFlightAt && f.lastAsk <= block.timestamp` is not enough on its own since the forged inFlightAt is also in the past by delivery time, so prefer the flag).","line":273,"path":"src/OracleAsker.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\nimport {SwarmRelay} from \"src/SwarmRelay.sol\";\nimport {OracleAsker} from \"src/OracleAsker.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\nimport {\n    APPROVED_OPERATOR,\n    INTAKE,\n    ORACLE_ACTION,\n    ATTESTATION_RELAYER,\n    ASK_MIN_INTERVAL,\n    ASK_TIMEOUT\n} from \"src/DeploymentConfig.sol\";\n\n/// @dev A concrete SwarmFeed whose attester key the test holds (same shape as test/helpers/ConfigurableSwarmFeed).\ncontract ProofFeed is SwarmFeed {\n    constructor(address attester_, address relayer_, uint256 maxAge_, uint256 cap_)\n        SwarmFeed(attester_, relayer_, 1, 3, maxAge_, cap_)\n    {}\n\n    function seed(uint256 value) external {\n        _accept(value, uint64(block.timestamp));\n    }\n}\n\n/// @dev The Intake as PR #66 behaves: collects the price, records the callback, completes with a 200k stipend.\ncontract ProofIntake {\n    mapping(bytes32 => mapping(address => uint256)) public priceOf;\n    mapping(bytes32 => IIntake.Callback) public callbackOf;\n    uint256 public nonce;\n\n    function setPrice(bytes32 action, address asset, uint256 amount) external {\n        priceOf[action][asset] = amount;\n    }\n\n    function request(bytes32 action, bytes calldata, IIntake.Callback calldata callback, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 requestId)\n    {\n        uint256 price = priceOf[action][asset];\n        require(price != 0 && amount >= price, \"not sold\");\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        requestId = keccak256(abi.encode(block.chainid, address(this), ++nonce));\n        callbackOf[requestId] = callback;\n    }\n\n    function complete(bytes32 requestId, bytes calldata args) external returns (bool delivered) {\n        IIntake.Callback memory c = callbackOf[requestId];\n        (delivered,) = c.target.call{gas: 200_000}(bytes.concat(c.selector, args));\n    }\n}\n\n/// @notice OracleAsker.onOracleResult decides \"the Treasury paid\" by `f.lastAsk == f.inFlightAt`. A refused\n/// Treasury purchase writes lastAsk = now + ASK_TIMEOUT - ASK_MIN_INTERVAL, a FUTURE timestamp; an `askPaid`\n/// sent in the block with exactly that timestamp writes inFlightAt equal to it, so when the Intake delivers\n/// that caller-paid request and the feed refuses it (the caller hand-relayed the same bytes first) the\n/// callback treats it as the Treasury's own refusal and pushes lastAsk another two hours out. Repeating\n/// that every cycle holds the Treasury off indefinitely for 0.5 IMD per two hours: the state the final\n/// panel audit's low was meant to close.\ncontract BackOffForgeryTest is Test {\n    uint256 private constant ATTESTER_KEY = 0xA11CE;\n    uint256 private constant PRICE = 0.5 ether;\n    address private constant ATTACKER = address(0xA77);\n    bytes private constant HEALTH_BODY = '{\"question\":\"network health\"}';\n\n    MockIMD private imd;\n    ProofIntake private intake;\n    ProofFeed private healthFeed;\n    OracleAsker private asker;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(10 days);\n        vm.roll(1_000);\n        vm.etch(ATTESTATION_RELAYER, address(new SwarmRelay()).code);\n        vm.etch(INTAKE, address(new ProofIntake()).code);\n        intake = ProofIntake(INTAKE);\n        imd = new MockIMD();\n        intake.setPrice(ORACLE_ACTION, address(imd), PRICE);\n\n        healthFeed = new ProofFeed(vm.addr(ATTESTER_KEY), ATTESTATION_RELAYER, 1 days, 2000);\n        address[] memory feeds = new address[](1);\n        feeds[0] = address(healthFeed);\n        bytes32[] memory hashes = new bytes32[](1);\n        hashes[0] = keccak256(HEALTH_BODY);\n        bool[] memory tracks = new bool[](1);\n        bool[] memory keepAlive = new bool[](1);\n        keepAlive[0] = true;\n        asker = new OracleAsker(IERC20(address(imd)), feeds, hashes, tracks, keepAlive);\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(address(asker), 100 ether);\n        imd.mint(ATTACKER, 10 ether);\n        vm.stopPrank();\n        healthFeed.seed(0.9 ether);\n    }\n\n    function test_aCallerPaidRefusalTimedToTheBackOffReadsAsTreasuryPaidAndExtendsIt() public {\n        // The NHI keep-alive: 18h in, the Treasury buys. Its answer is refused (any refusal: here a bad\n        // signature; a hand relay of the same bytes before the callback does the same), so the back-off lands.\n        vm.warp(block.timestamp + 20 hours);\n        bytes32 r1 = asker.ask(address(healthFeed), HEALTH_BODY);\n        assertTrue(intake.complete(r1, abi.encode(r1, _attestation(r1, 0.9 ether), hex\"00\")), \"refused, callback completes\");\n        (,,, uint64 backedOffUntil,,,) = asker.feeds(address(healthFeed));\n        assertEq(backedOffUntil, uint64(block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL), \"the Treasury's own refusal backs it off\");\n\n        // ATTACKER waits for the block whose timestamp equals that future lastAsk and buys with its own IMD.\n        vm.warp(backedOffUntil);\n        vm.startPrank(ATTACKER);\n        imd.approve(address(asker), PRICE);\n        bytes32 r2 = asker.askPaid(address(healthFeed), HEALTH_BODY, PRICE);\n        vm.stopPrank();\n        (,,, uint64 lastAsk,, uint64 inFlightAt,) = asker.feeds(address(healthFeed));\n        assertEq(lastAsk, inFlightAt, \"a caller-paid request now carries the Treasury-paid signature\");\n\n        // The answer is public before the callback; ATTACKER relays it by hand, then the Intake delivers.\n        vm.warp(block.timestamp + 5 minutes);\n        SwarmFeed.OracleAttestation memory a = _attestation(r2, 0.9 ether);\n        bytes memory sig = _sign(a);\n        vm.prank(ATTACKER);\n        SwarmRelay(ATTESTATION_RELAYER).relay(healthFeed, a, sig);\n        assertTrue(intake.complete(r2, abi.encode(r2, a, sig)), \"refused as a replay, callback completes\");\n\n        // EXPECTED (the final panel audit fix, OracleAsker.sol:273-293): only the Treasury's own refused\n        // purchase backs the Treasury off, so a caller-paid refusal leaves lastAsk where it was.\n        (,,, uint64 after_,,,) = asker.feeds(address(healthFeed));\n        assertEq(after_, backedOffUntil, \"a caller-paid refusal must not extend the Treasury's back-off\");\n    }\n\n    function _attestation(bytes32 id, uint256 figure) private view returns (SwarmFeed.OracleAttestation memory a) {\n        a.requestId = id;\n        a.chainId = 1;\n        a.questionHash = keccak256(\"q\");\n        a.answerType = 3;\n        a.answer = abi.encode(figure);\n        a.figure = figure;\n        a.fromBlock = 100;\n        a.toBlock = 200;\n        a.blockHash = keccak256(\"b\");\n        a.panelJobId = keccak256(\"panel\");\n        a.panelSize = 60;\n        a.quorum = 20;\n        a.agreed = 40;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 1 hours);\n    }\n\n    function _sign(SwarmFeed.OracleAttestation memory a) private view returns (bytes memory) {\n        bytes32 body = keccak256(\n            bytes.concat(\n                abi.encode(\n                    healthFeed.ATTESTATION_TYPEHASH(), a.requestId, a.chainId, a.questionHash, a.answerType,\n                    keccak256(a.answer), a.figure\n                ),\n                abi.encode(\n                    a.fromBlock, a.toBlock, a.blockHash, a.panelJobId, a.panelSize, a.quorum, a.agreed,\n                    a.issuedAt, a.expiresAt\n                )\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) =\n            vm.sign(ATTESTER_KEY, keccak256(abi.encodePacked(\"\\x19\\x01\", healthFeed.DOMAIN_SEPARATOR(), body)));\n        return abi.encodePacked(r, s, v);\n    }\n}","reproduction":"Fixture as test/OracleAsker.t.sol (MockIntake at INTAKE selling oracle.request for 0.5 IMD, SwarmRelay at ATTESTATION_RELAYER, a 1-day/2000-bps feed seeded at 0.9e18, asker holding 100 IMD, keepAlive true). T0+20h: anyone calls ask(healthFeed, body) -> R1 (lastAsk = inFlightAt = T1). The Intake completes R1 with bytes the feed refuses (bad signature here; a hand relay of the same attestation before the callback gives ReplayedAttestation on mainnet): Delivered(relayed=false), feeds(healthFeed).lastAsk == T1 + 6600 (the Treasury's own back-off, expected). ATTACKER warps to exactly T1 + 6600, approves 0.5 IMD and calls askPaid(healthFeed, body, 0.5e18) -> R2: inFlightAt == lastAsk == T1 + 6600. Five minutes later ATTACKER relays R2's attested answer by hand (accepted), then the Intake completes R2 with the same bytes: the callback's relay reverts ReplayedAttestation. EXPECTED (OracleAsker.sol:266-293, 'only the Treasury's own live purchase holds the Treasury back'): lastAsk unchanged at T1 + 6600 = 942600 in the test. ACTUAL: lastAsk == 949500 = (T1 + 6600 + 300) + 6600; the caller-paid refusal extended the back-off by another two hours, and ask() reverts TooSoon until then. test/scratch/BackOffForgery.t.sol fails on this code with '949500 != 942600' and passes once the catch keys on an explicit Treasury-paid flag.","severity":"low","snippet":"        bool treasuryPaid = live && f.lastAsk == f.inFlightAt;","title":"OracleAsker.onOracleResult: the Treasury-paid test (lastAsk == inFlightAt) is forged by an askPaid sent in the block whose timestamp equals a prior back-off's future lastAsk, so a caller-paid refusal "},{"citation":"resolved","description":"The bound is exactly as `_checkValue`/`_epoch` state it: every value accepted within maxAge of an epoch's open lies within the allowance of the ANCHOR, and the next epoch anchors at whatever value was last accepted. Nothing holds the last value of one epoch and the first of the next apart in time, so a buyer who joins at the end of an honest epoch lands 1.2V in the epoch's last block and 1.44V in the next block (the boundary), then 1.2 per hour. The numbers the doc gives (2.07x at R+3h from the honest value at R) are right; the sentences at lines 30 and 49 and docs/PARAMETERS-2026-10-05.md 'a run of steps compounds at the cap per hour after the first' are not: measured from the attacker's first step, 2.07x is two hours and one block, and over the single hour [R, R+1h] the feed moved 44%. The precise statement is 'within one epoch, within the allowance of the anchor; consecutive epochs compound'. Documentation only; no change to the economics table beyond the first hour.","line":30,"path":"src/SwarmFeed.sol","reproduction":"StepFeed (SwarmFeed, maxAge 1h, cap 2000): seed V at R; at R+3588 (the last block of the epoch) accept 1.2V; at R+3600 (the next block) accept 1.44V, EXPECTED per line 30 refused as a second step inside one hour, ACTUAL accepted (new epoch anchored at 1.2V with the cap); 1.728V at R+2h and 2.0736V at R+3h then land as the doc says. test/scratch/BoundaryTwoStep.t.sol passes on this code and shows the sequence.","severity":"info","snippet":"/// now a feed anyone keeps alive moves at most the cap per hour however it is driven. An epoch opened","title":"SwarmFeed NatSpec: 'moves at most the cap per hour however it is driven' / 'compounds at no more than the cap per hour after the first' is per epoch, not per hour: two steps straddle an epoch boundary"},{"citation":"resolved","description":"Question 4. verifySeeded() reads the three feeds and the pool and refuses a price or spot first value more than 5% from the pool, or outside SKEW_BPS of each other, or an NHI outside (0, 1e18]. It is a view run by the operator; nothing on chain reads it. The vault is permissionless from construction, the feed addresses are CREATE2 constants computable from source, the Intake bodies are public and name the planned feed as consumer, and SwarmRelay forwards for anyone, so a prepared relayer can buy attestations for the planned addresses before the feeds exist (the attester signs for the consumer address in the body; a window must close within 300 blocks of the relay) and relay them in the block after `_deploy(SALT_PRICE..)`, ahead of the operator's purchase in runbook 7.1, then lock sIMD and draw against the raced price in the same block as the vault lands. The check then fails and the operator does not announce, but the draw has happened: at a 2x anchor the honest value is refused for six hours (allowance must reach 5,000 bps: 4,000 + 250*(p-1) with p = 5) and the vault prices every position at 2x meanwhile; at k times the market the gain is LINE - 1.7*LINE/k if imdUSD can be sold at par, against the cost of holding the pool pumped through a 13-sample window at an unknown deployment time. The panel rated the race low and the chosen fix is detection; this records that the detection does not keep a raced value from pricing any deposit, so the NatSpec at SwarmFeed.sol:372-375 and DeployMainnet.s.sol:341-349 overstate it. Smallest closing fix (the panel's other option): buy the three attestations for the planned addresses before the broadcast and relay them in the same transaction as each feed's deployment (or deploy the vault only after verifySeeded passes, which run() does not do today).","line":342,"path":"script/DeployMainnet.s.sol","reproduction":"Mainnet sequence: (1) attacker computes p.price/p.spot/p.nhi from source (plan()), buys price and spot attestations for those consumers with windows closing within the last 300 blocks while holding the pool at 2x through 7 of 13 samples, and an NHI attestation; (2) DeployMainnet.run() broadcasts; in the block after the feeds land the attacker calls SwarmRelay.relayMany with the three attestations: all accepted (first values are unbounded, test/scratch/FirstValueRace.t.sol in the panel record); (3) in the block after the vault lands the attacker calls lockIMD and draw up to LINE at the 2x price (fresh feeds, spot within SKEW_BPS of primary, CR >= 170 at 2x); (4) the operator's honest attestations revert ExcessDeviation (|V - 2V| > 20% of 2V) and verifySeeded fails 'first value is off the pool'. EXPECTED per the NatSpec: the raced value is caught before any deposit is priced; ACTUAL: the racer's deposit was priced at 2x in step (3), and the honest value cannot land until 6 hours after the raced relay.","severity":"info","snippet":"    /// refuse a first value someone else raced in. Nothing on chain bounds a feed's first value and the","title":"DeployMainnet.verifySeeded / SwarmFeed NatSpec: 'a raced first value is caught before deposits open' is a runbook convention, not a gate: ParameterizedVault accepts lock and draw from its constructor,"},{"citation":"resolved","description":"The slot-3 repack in 8756817 narrowed `_anchorBound` from uint32 to uint24 (line 144, '@dev At most MAX_ALLOWANCE_BPS (1e6), so 24 bits are exact'), and `_accept` casts with uint24(bound). The constant's own NatSpec still names uint32. Every cast was checked and is exact: uint24(maxDeviationBps) <= 10_000, uint24(bound) <= 1_000_000 < 16_777_216, uint40(block.timestamp), uint80(value) only when value <= type(uint80).max, uint64(_anchorAt); 64+8+40+24+40+80 = 256 bits. Documentation only.","line":114,"path":"src/SwarmFeed.sol","reproduction":"Read src/SwarmFeed.sol:113-115 against line 144 and line 478 (`uint24(bound)`). EXPECTED per line 114: a uint32 field; ACTUAL: `uint24 private _anchorBound`.","severity":"info","snippet":"    /// packed uint32 exact.","title":"SwarmFeed.MAX_ALLOWANCE_BPS NatSpec says it keeps the packed uint32 exact; the field has been uint24 since 8756817 (1e6 < 2^24 still holds, the sentence is stale)"},{"citation":"resolved","description":"Recomputed offline: keccak256(QUESTION_PREFIX || '26120928' || ',\"toBlock\":' || '26121526' || '}}') == 0xc87aa8a9fa49ca4885e3c3048e9159cd00578e7ad37188b1fce70199c510c16e, the questionHash the attester signed in oracle/attestation-e2c85027.json (window 26120928..26121526, figure 3729511079526129). So PriceFeed's pinned prefix is verified against a live signature and the sentence at lines 36-38 is stale in the pessimistic direction. The same canonicalisation (oracle/question-prefix.mjs) applied to deploy/mainnet/bodies/{price,spot,nhi}.template.json reproduces each feed's QUESTION_PREFIX byte for byte (3466/3244/3910 hex chars), which is what deploy/mainnet/check-bodies.mjs asserts. SpotFeed.sol:43 and NhiFeed.sol:36 still say 'Nothing has been bought with it yet' while docs/LAUNCH-READINESS.md says live NHI and SPOT attestations were proven on testnet but their request ids are not archived in this repository. Documentation and launch-record item: archive those two request ids (or re-verify with --verify before the freeze) and reword the three NatSpec passages.","line":38,"path":"src/PriceFeed.sol","reproduction":"python3 -I: prefix = bytes.fromhex(PriceFeed.QUESTION_PREFIX); doc = prefix + b'26120928,\"toBlock\":26121526}}'; cast keccak <doc> prints 0xc87aa8a9...c510c16e, equal to message.questionHash in oracle/attestation-e2c85027.json. EXPECTED per the NatSpec: no live attestation checks the constant; ACTUAL: one in the repository does and matches.","severity":"info","snippet":"    /// yet, so this constant has NOT been checked against a live attestation.","title":"PriceFeed/SpotFeed/NhiFeed NatSpec say the pinned question prefix has not been checked against a live attestation; the PriceFeed prefix reproduces the questionHash of the archived live attestation e2c"},{"citation":"resolved","description":"8756817 rewrote SwarmFeed.submitAttestation's NatSpec (lines 228-239) to say the relayer is not a trust boundary on the shipped feeds and that the first value is bounded by nothing on chain. DeploymentConfig.sol:62-64 still cites the old sentence as the reason the relayer must be nonzero, and lines 66-69 say the relay is 'deployed to Sepolia at the address below' although the mainnet plan (deploy/mainnet/plan.py) rewrites the address to the planned CREATE2 SwarmRelay before the broadcast and `_refuseUnlessReady` refuses to deploy otherwise. The constructor's own comment (SwarmFeed.sol:187-205) states the real property. Documentation only.","line":63,"path":"src/DeploymentConfig.sol","reproduction":"Read src/DeploymentConfig.sol:62-64 against src/SwarmFeed.sol:233-238. EXPECTED per line 63: submitAttestation documents permissionless relay as unsafe; ACTUAL: it documents that the relayer 'is not a trust boundary on the shipped feeds: it is SwarmRelay, which forwards for anyone'.","severity":"info","snippet":"/// which SwarmFeed.submitAttestation documents as unsafe for as long as questionHash binds a","title":"DeploymentConfig ATTESTATION_RELAYER NatSpec still says a zero relayer would be 'permissionless relay, which SwarmFeed.submitAttestation documents as unsafe'; submitAttestation now documents the oppos"}],"hash":"bdbc9d0066821066303a092e874ff3008393a9aced7d33df26d8e85347877c41","nodeId":"d33c5fe6-3ed9-4d77-a927-78501633acdc","outcome":"completed","summary":"The review is complete. No tracked file changed; the findings are in `.imd-findings.json` (one low with a Foundry proof, five infos), and the scratch tests live under `test/scratch/`.\n\n## Result\n\nThe newest fixes hold. The repacked slot 3 and every cast are exact, silence measured from the later of signature and relay closes the held-attestation walk, and the per-epoch bound compounds at the cap per epoch in both directions. One fix left a gap: the Treasury-paid test in the asker callback can be forged.\n\n**Low (with proof): back-off forgery in OracleAsker.onOracleResult.** The callback infers \"the Treasury paid\" from `lastAsk == inFlightAt`. A refused Treasury purchase writes `lastAsk` to a timestamp 6,600 s in the future, a multiple of the 12 s slot. An `askPaid` sent in that exact block writes `inFlightAt` equal to it, so when that caller-paid answer is refused (the attacker hand-relays the same bytes first) the callback extends the back-off by another two hours. Repeated, this holds Treasury-paid asks off indefinitely for 0.5 IMD per cycle, the state the final panel's low was meant to close. The first back-off can be forced for free by front-running the Intake callback for any Treasury ask. Nothing is mispriced; the keeper must pay instead. Fix: an explicit paid-by flag rather than a timestamp match. The proof in `test/scratch/BackOffForgery.t.sol` fails on this code and passes with the flag, with the OracleAsker suites still green.\n\n## The numbered questions\n\n1. **Attestation acceptance is sound.** Domain binds chain id and feed address, so no cross-feed or cross-chain replay. `requestId` is consumed per feed. `issuedAt` is bounded below by the last accepted value and by maxAge, above by now and `expiresAt`. Floors 25/15 enforced. The question hash is rebuilt from the pinned prefix and the signed window, with span, advance and 300-block recency bounds. I recomputed the PriceFeed prefix against the archived live attestation e2c85027 and it reproduces the signed questionHash. All three prefixes equal the canonical prefix of the deploy bodies.\n\n2. **The bound.** Packing is 64+8+40+24+40+80 = 256 bits; casts are exact. Largest step after H hours of silence from the relay: 2,000 bps under two hours, then 4,000 + 250·(H−2), capped at 1e6; for NHI the same from H = 25. Any gap is eventually followed. The fastest sustained rate is the cap per epoch, not per hour: two steps straddle a boundary one block apart (1.44x in twelve seconds), then 1.2x per hour, symmetric in both directions; NHI 1.2x per day. Holding an attestation no longer helps. I found no sequence faster than that.\n\n3. **OracleAsker.** Nobody can make the Treasury pay without a cause the chain shows, exceed the daily budget, or lose a paid answer that the feed would accept. Holding the Treasury off is possible via the forgery above.\n\n4. **The first value.** `verifySeeded` is a view the operator runs; the vault accepts `lock` and `draw` from its constructor. A prepared relayer can buy attestations for the planned CREATE2 addresses before the feeds exist, relay in the block after deployment, and draw against the raced price before anyone runs the script. A failed check costs up to six hours at a 2x anchor during which that price stands. Recorded as info since the panel's low was accepted with detection as its fix.\n\n5. **Dead legs degrade correctly.** A reverting or malformed Chainlink or share-vault leg reads as zero and stale. Gated actions halt, `lock` and `wipe` keep the secured term (the new fix works), `cover` requires fresh feeds, the Treasury values the asset at nothing.\n\n6. **The walk.** With the attacker's collateral sized so the draw hits the $1M line, gain is 1M − 1.7M/k: about $16k at 1.73x (one hour), $179k at 2.07x (two hours), $525k at 3.58x (five hours), against roughly $20k–$41k of pool fees plus absorbing every holder who sells into the pump for the samples of each rung. What stops it at launch is the exit: imdUSD has no market, and redemption pays bac","treeHash":null,"usage":{"cachedInputTokens":4232858,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":86925,"runtime":"claude","turns":45,"wallClockMs":1418913}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa2b0b9c7ebc1547","findings":[{"citation":"resolved","description":"Q4. The final panel's low (first value unbounded, relay permissionless) was answered with an off-chain check, verifySeeded(), run in runbook 7.1 AFTER the broadcast. But run() deploys the three feeds (layer 2) and the ParameterizedVault (layer 5) in one broadcast and then calls verify(), which REQUIRES every feed to be stale (line 298), so by construction there is a window in which the vault is live, lock/lockIMD/draw are open to anyone (CDPVault has no pause and no deposits-open switch; Parameters changes take 48 hours), and the first accepted value of PriceFeed and SpotFeed belongs to whoever relays first through SwarmRelay: SwarmFeed._checkValue applies no bound while _hasValue is false. The attacker needs attester-signed attestations for a pumped window. The feed addresses are pure functions of the source (plan()), the bodies are public with {{FEED}} filled from the plan, the attester signs for the consumer address named in the body, and _requireQuestion accepts a window that closed up to 300 blocks before the relay, so the attestations can be bought in the hour before the relay. Holding IMD's $2.3M-a-side pool at 2x for 150 of a 300-block window (7 of the 13 median samples) costs about $953k of ETH in and back out at the pool's 1% (about $19k of fees) plus whatever arbitrageurs sell into the pump: the ramp-and-hold PARAMETERS already costs. Once the raced 2x anchors price and spot (NHI needs only any honest value), the attacker locks sIMD worth $1M at market and draws the whole LINE ($1M) at a 200% ratio measured on 2x, in the block after the feeds exist. verifySeeded() then fails as designed, but its only remedy is 'wait for the allowance to widen and relay honest values': an honest value at 0.5x the anchor is refused ExcessDeviation until _allowanceNow reaches 5,000 bps, six hours of silence after the attacker's relay, and the position cannot be undone. At the market the position sits at 100%. The attacker marks it themself, waits the six-hour grace, and bites 833,333 imdUSD of the debt to take all 100,000 IMD (the 20% bonus is their own collateral): they end with their collateral back (less the protocol's 10% of the bonus) plus ~$167k of imdUSD that nothing backs, and the protocol with ~$167k of debt behind sub-dollar dust, realized as bad debt once cover sweeps it. Sized at mat instead (collateral $850k at market, ratio 170% on 2x), the unbacked remainder is ~$292k. With no imdUSD market at launch the imdUSD is not immediately sellable, but it is a permanent claim on the protocol's reserve and on every later borrower's collateral through cash, and the LINE is consumed for honest borrowers until the position is covered. SwarmFeed._checkValue NatSpec (lines 372-375) says the raced value 'is caught before deposits open'; deposits are open from the vault's constructor. Reachable with the constants as committed; what the attacker must win is the race between the feeds' deployment and the deployer's own relay (the runbook buys only after the broadcast, so the race is the panel's latency plus the operator's reaction, against an attacker who can pre-buy if the deploy time is known within an hour). Smallest fix: buy the three seed attestations for the planned CREATE2 addresses BEFORE the broadcast and relay them in the same broadcast immediately after layer 2 (before the vault), and replace verify()'s `require(f.isStale(), ...)` with verifySeeded's bands, so there is no block in which the vault is live on unseeded feeds. Alternatively deploy the vault in a second broadcast only after verifySeeded() has passed.","line":298,"path":"script/DeployMainnet.s.sol","reproduction":"test/scratch/FirstValueRace.t.sol (passes on this code: it demonstrates the state). ParameterizedVault over MockIMD with three RaceFeeds (relayer = SwarmRelay at ATTESTATION_RELAYER, data chain 1, type 3, cap 2000; price/spot 1h, NHI 1d), Chainlink etched at 2500e8, TreasuryFactory etched; all three feeds stale, as verify() requires. STRANGER relays attester-signed first values: price 0.008 ETH (2x the 0.004 market, $10/IMD), spot 0.008, NHI 0.9; then lock(100_000e18) and draw(1_000_000e18) in the same block. EXPECTED (runbook 7.1, SwarmFeed NatSpec 372-375): no deposit is priced by a raced first value. ACTUAL: draw succeeds, collateralRatio == 200. DEPLOYER's honest price attestation at 0.004: reverts ExcessDeviation now and at +5h59m; accepted at +6h00m01s, after which collateralRatio == 99 and totalDebt == 1_000_000e18 with LINE full. STRANGER then bark(self), +6h, DEPLOYER relays fresh market price/spot, STRANGER bite(self, 833_333e18): STRANGER's IMD rises by > 98,000e18, STRANGER keeps > 166,000e18 imdUSD, the position holds < 1e18 collateral against > 166,000e18 of debt.","severity":"medium","snippet":"            require(f.isStale(), \"feed: must open unseeded\");","title":"DeployMainnet.verify() requires the feeds unseeded while the vault is already live, so a raced first value prices deposits before verifySeeded() can run; a 2x race lets the racer draw the LINE, self-l"},{"citation":"resolved","description":"Q3. The final panel's low (a hand-relayed askPaid answer backed the Treasury off) was fixed by counting a refusal only when `f.lastAsk == f.inFlightAt`, on the reasoning (lines 266-272) that only `ask` writes both in one call. Two gaps. (1) The catch itself writes lastAsk = block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL (line 293), a FUTURE timestamp 6,600 s ahead; 6,600 = 550 x 12, so on mainnet, whose block timestamps are genesis + 12 x slot, that instant is itself a slot timestamp. In that block `ask` is still refused (TooSoon until lastAsk + 10 min) but `askPaid` is not, and it writes inFlightAt == lastAsk. A caller-paid request made in that block therefore passes the Treasury-paid test; when its delivery is refused (the buyer hand-relays the public answer first, ReplayedAttestation) the catch writes a fresh two-hour back-off. Each cycle costs the attacker 0.5 IMD and lands exactly on the next 6,600-second boundary, so it chains: the Treasury buys no fall, keep-alive or wide-open refresh while the chain runs, the outcome the panel's low described, for ~$60 a day. (2) The condition is met, by design, for the Treasury's own purchase, but a STRANGER can cause that refusal for the price of gas: SwarmRelay admits everyone and the plane's attestation is public before the callback lands, so relaying the Treasury's own answer first makes the callback's relay revert ReplayedAttestation and backs the Treasury off two hours although its answer landed and the feed is fresh; a later-window askPaid answer landing first (WindowNotAdvancing / StaleAttestation) does the same, and (2) is also how a chain under (1) is started without waiting for an honest refusal. Harm is bounded as the panel rated it: nothing is mispriced, the keeper's askPaid fallback still works, the feed goes stale within the hour anyway; but the fix's stated property ('only the Treasury's own purchase backs the Treasury off', lines 269-272 and 290-293) does not hold. Smallest fix: do not back off on refusals that mean the answer already landed or was overtaken (match ReplayedAttestation, WindowNotAdvancing and StaleAttestation selectors in the catch), or record `treasuryPaid` explicitly in the Feed struct (set in ask, cleared in askPaid/askPaidMany) instead of inferring it from timestamp equality.","line":273,"path":"src/OracleAsker.sol","reproduction":"test/scratch/BackoffSpoof.t.sol (passes on this code: it demonstrates the state). Fixture as test/OracleAsker.t.sol (MockIntake at INTAKE, SwarmRelay at ATTESTATION_RELAYER, pool mock, a 1h/2000 feed seeded at 0.001 ETH, asker holding 15 IMD, tracksPool), block.timestamp = 1_700_000_003 (11 mod 12, a mainnet slot time). Pool set to a 10% fall, arm, +5 blocks, ask() -> R1; the Intake completes R1 with a bad signature: lastAsk == T+6600 (documented back-off) and (T+6600) % 12 == 11, a slot timestamp. warp(T+6600): ATTACKER askPaid(feed, body, 0.5e18) -> R2; feeds(feed).lastAsk == inFlightAt. +5 min: ATTACKER relays R2's attestation by hand (accepted), the Intake completes R2 with the same bytes. EXPECTED (lines 269-272): lastAsk unchanged at T+6600. ACTUAL: lastAsk == now + 6600, and ten minutes later with the pool at a further 11% fall, armed and still present, ask() reverts TooSoon. Second test: Treasury ask() -> R1, STRANGER relays R1's own attestation first, the Intake completes R1: lastAsk == now + 6600 although the honest answer landed.","severity":"low","snippet":"        bool treasuryPaid = live && f.lastAsk == f.inFlightAt;","title":"OracleAsker.onOracleResult's Treasury-paid test (lastAsk == inFlightAt) is satisfied by a caller-paid askPaid sent in the block at exactly the back-off end, and the Treasury's own answer can be made '"},{"citation":"resolved","description":"Q5. UsdPriceFeed documents (lines 18-22) and SharePriceFeed repeats (lines 28-34) that a missing, paused or malformed aggregator answer reads as zero so that no consumer reverts; _ethUsd() refuses non-positive answers, zero or over-uint64 timestamps and more than 77 decimal places for exactly that reason. It does not bound the answer's magnitude. latestRoundData's answer is an int256; for any positive answer with imdEth x answer >= 2^256 x 10^decimals, Math.mulDiv reverts (OpenZeppelin v5 MathOverflowedMulDiv) because the RESULT does not fit, and the revert propagates: UsdPriceFeed.latestValue, ethUsdPrice and SharePriceFeed.latestValue all revert while isStale() still answers false (it reads the same answer as well formed). Consumers: ParameterizedVault._priceOrZero -> collateralPriceFeed.latestValue(), which is read on the ungated paths that promise never to revert (lock, lockIMD, wipe via _reduceDebt -> _resecure, cover's `last = _priceOrZero()`), so a borrower can neither repay nor add collateral while the leg answers that way, and _clearIfRecovered cannot run; the Treasury's reserve valuation is unaffected (it uses _boundedCall). At the committed scale (imdEth ~4e15, 8 decimals) the threshold is an answer above ~2.9e69, versus a real answer of ~2.5e11: not a market condition, but exactly the 'malformed' class the two feeds say they absorb, and the same class (over-uint80 round ids, over-77 decimals) that earlier fixes in this file bounded. Smallest fix: in _ethUsd, refuse an answer that cannot be a price, e.g. `if (uint256(answer) > type(uint256).max / 1e36) return (0, 0, 0);` (far above any real price, far below the overflow point), so the documented zero is what every consumer sees.","line":43,"path":"src/UsdPriceFeed.sol","reproduction":"test/scratch/UsdLegOverflow.t.sol (passes on this code: it demonstrates the state). Aggregator etched at CHAINLINK_ETH_USD with 8 decimals; IMD/ETH leg fixed at 4e15 (never stale); UsdPriceFeed over it, SharePriceFeed over a vault answering convertToAssets(1e18) = 7.95e12. Answer 2500e8: latestValue == 4e15 * 2500e8 / 1e8 (correct). Answer 1e70, fresh timestamp: isStale() == false; EXPECTED per the NatSpec latestValue() == (0, 0); ACTUAL usd.latestValue(), share.latestValue() and usd.ethUsdPrice() all revert (MathOverflowedMulDiv).","severity":"low","snippet":"        value = Math.mulDiv(imdEth, ethUsd, 10 ** decimals);","title":"UsdPriceFeed.latestValue (and SharePriceFeed through it) reverts instead of reading zero when the ETH/USD answer is oversized, contradicting the 'anything unreadable reads as zero' contract that lock/"},{"citation":"resolved","description":"Documentation only, listed as the task asks. (a) src/SwarmFeed.sol:113-114: the allowance 'keeps the packed uint32 exact' - _anchorBound is uint24 after the slot-3 repack (line 144 says so correctly; 1e6 < 2^24 = 16,777,216 so the cast is still exact). (b) src/SwarmFeed.sol:21: 'Zero is rejected on both paths' - there is one path since the reporter fallback was removed. (c) src/DeploymentConfig.sol:62-64: ATTESTATION_RELAYER 'Zero would mean permissionless relay, which SwarmFeed.submitAttestation documents as unsafe' - submitAttestation's NatSpec (lines 233-236) now says the relayer is not a trust boundary on the shipped feeds and that SwarmRelay forwards for anyone; the constant's comment still describes the pre-epoch reasoning. (d) src/SwarmFeed.sol:372-375 'caught before deposits open' and src/OracleAsker.sol:266-272 'only the Treasury's own purchase backs the Treasury off' are covered by the medium and the first low above. Packing and casts checked (Q2): slot 3 = uint64 _updatedAt + bool _hasValue (8) + uint40 _anchorAt + uint24 _anchorBound + uint40 _acceptedAt + uint80 _epochFirst = 256 bits exactly; uint24(bound) with bound <= 1e6, uint40(block.timestamp), uint80(value) guarded by `value <= type(uint80).max`, uint64(_anchorAt) in epoch(): all exact.","line":114,"path":"src/SwarmFeed.sol","reproduction":"Read the cited lines against src/SwarmFeed.sol:142-144 (uint24 _anchorBound) and src/SwarmFeed.sol:233-236.","severity":"info","snippet":"    /// packed uint32 exact.","title":"NatSpec claims the code does not have: MAX_ALLOWANCE_BPS 'keeps the packed uint32 exact' (the field is uint24 since 8756817), 'rejected on both paths' (one path), and the stale relayer cross-reference"},{"citation":"resolved","description":"Q1 (nothing wrong): the EIP-712 domain binds block.chainid and address(this), so an attestation signed for PriceFeed is refused by SpotFeed and by any other chain; usedRequests is per feed; issuedAt must be <= now, <= expiresAt, not older than maxAge and not older than the last accepted value; panelSize >= 25 and 15 <= agreed <= panelSize; the question hash is recomputed from the pinned prefix and the SIGNED fromBlock/toBlock, the span is bounded (300..1200 price, 150..1200 spot/NHI, and the pinned bodies resolve to 600/300/300 blocks), toBlock must advance past lastToBlock, and on chain 1 (every mainnet feed) must be <= block.number and at most maxAge/12 blocks old. No attestation for a different question, chain, feed or window is accepted; a hold is bounded to maxAge by both issuedAt and the window. Q2 (nothing wrong beyond the packing note): an epoch opens at the relay block and lasts maxAge; every value in it is within the stored bound of the anchor (the value held when it opened); the next epoch anchors at the last accepted value, so from a fresh feed the sustained rate is (1+cap) per maxAge in either direction: 1.2 per hour for price/spot (2.07x at T0+3h, 3.48x at T0+6h; 0.8/h down), 1.2 per DAY for NHI (its epoch is a day). Silence from the LATER of signature and relay (line 436) earns the stale base only after a whole hour past the lifetime, so the largest single step after H hours of silence is 20% for H < 2, else 4000 + 250(H-2) bps (40% at 2h, 50% at 6h, 60% at 10h, 100% at 26h, 100x at ~3,986h); NHI the same with H-25 (40% at 25h). A stale opening is strictly slower than the cap per hour (1.4 per 2h < 1.44; 1.45 per 4h < 2.07), every acceptance resets the silence, holding an attestation back cannot help (since >= relay time), and a wide epoch holds later values to the cap around its first, so no sequence beats the cap per hour after the first step. Every gap is followed: a 99.99% fall is accepted at 26h, a 100x rise at ~166 days and larger rises in further 20%/h steps, with the vault paused (StaleFeed) meanwhile as PARAMETERS tabulates. Q3: ask/askPaid/askPaidMany/_request/onOracleResult are as the NatSpec says except for the back-off gap reported above; nobody can exceed the daily budget (the asker holds at most one budget, one request in flight per feed, 10-minute interval, price ceiling), nobody can lose a paid answer (feedOf survives a timeout, a refused answer stays public), and the only hold-off is the back-off chain reported (low). Q4: reported (medium). Q5: units are right throughout (imdEth x ethUsd / 10^dec = USD per 1e18 raw IMD; rate x assetUsd / 1e18 = USD per 1e18 raw sIMD, ~7.95e13 at $10/IMD, which CDPVault._collateralRatio handles through its remainder path since price < 1e16); a reverting, short or malformed Chainlink or share-vault answer reads as (0,0) and stale: gated vault paths revert StaleFeed, lock/lockIMD/wipe keep the secured term (8756817 fix verified), cover with a zero last price requires fresh feeds, _clearIfRecovered preserves the mark, the Treasury counts the asset for nothing; the one exception is the oversized-answer revert reported (low). Q6, at LINE $1M, mat 170, cap 2000, pool $2.3M a side at 1%: the walk up to 3.48x is seven rungs over six hours from a fresh feed (six over five from a two-hour-silent one), each rung a ramp-and-hold of 150 of 300 blocks (7 of 13 median samples) for the primary plus a single pushed block for spot; fees about $40k cumulative to 3.48x (the per-rung round trip grows with the rung) plus unbounded exposure to holders selling into the pump; prize: draw $1M against collateral worth $1M/1.7/3.48 = $169k at market, then self-liquidate after the correction as in the medium, keeping ~$830k of imdUSD unbacked. Down: 0.8/h; two rungs (0.64) put every position at mat under water; the bonus is 20% of debt repaid, at most ~$200k on the whole LINE, minus the loss on reselling seized sIMD into the dumped pool, and it needs the dump held through the six-hour grace","line":436,"path":"src/SwarmFeed.sol","reproduction":"Not a defect; the arithmetic above is checked against SwarmFeed._allowanceNow (lines 428-443), _accept (466-490), _checkValue (388-404) and the regression tests test_relayingAnHourApartNeverEarnsTheStaleBase / test_aHeldAttestationDoesNotEarnTheStaleBase, which pass on this tree.","severity":"info","snippet":"        uint256 since = _acceptedAt > _updatedAt ? _acceptedAt : _updatedAt;","title":"Numbered answers and coverage: acceptance sound; the per-epoch bound holds at the cap per hour (1.2/h price feeds, 1.2/day NHI) with the first step after H hours of silence 40% + 2.5%(H-2) (NHI: H-25)"}],"hash":"f0c636da07c3ce9409b782cadf84b9802de2c172b78a153d973305785197b2b8","nodeId":"875ac1c9-a49c-412b-93e0-546302eb0b9d","outcome":"completed","summary":"The review is complete. The findings file holds 1 medium, 2 lows and 2 info notes, every snippet validated against the tree, and the three scratch tests reproduce the states described.\n\n**Findings**\n\n- **Medium, `script/DeployMainnet.s.sol:298`.** The deploy broadcast puts the vault live and then requires every feed to be unseeded, so the first value belongs to whoever relays first, and deposits are open before the off-chain `verifySeeded` can run. A racer who relays a 2x price and spot can draw the whole $1M line in the next block, and the deployer's honest value is refused for six hours. After the correction the racer self-liquidates and keeps roughly $167k of unbacked imdUSD (about $292k when sized at mat). The fix is to buy the seed attestations for the planned addresses before the broadcast and relay them in it, with the check in `verify()` turned into the seeded bands.\n- **Low, `src/OracleAsker.sol:273`.** The new Treasury-paid test compares two timestamps. The back-off writes a future timestamp that is itself a mainnet slot time, so an `askPaid` in that exact block passes the test and re-arms the two-hour back-off, chaining for 0.5 IMD per cycle. A stranger hand-relaying the Treasury's own answer also triggers it for free. Nothing is mispriced, but the fix's stated property does not hold.\n- **Low, `src/UsdPriceFeed.sol:43`.** An oversized but positive Chainlink answer makes the composite revert instead of reading zero, which propagates into the ungated vault paths. Not a market condition, but the exact malformed class the file claims to absorb.\n- **Info.** Four stale NatSpec passages, and the numbered answers with coverage.\n\n**Numbered answers in brief**\n\n1. Acceptance is sound: domain, replay, both-sided issuedAt, panel floors and the spliced question hash with bounded, advancing, recent windows. Nothing from another question, chain, feed or window is accepted.\n2. Packing is exactly 256 bits and every cast is exact. Sustained rate is the cap per lifetime in both directions, so 1.2 per hour for price and spot and 1.2 per day for NHI. The first step after H hours of silence is 20% under two hours, then 40% plus 2.5% per further hour, capped at 100x. Holding gives nothing now, and no gap is left unfollowed.\n3. The asker is as documented except the back-off gap above. Budget, in-flight slot, interval and ceiling hold, and no paid answer is lost.\n4. The check detects but cannot prevent, since the vault is live before it runs, and a failed check costs six hours plus any position opened meanwhile.\n5. Units are right throughout, and a dead or malformed leg degrades to stale and zero everywhere except the overflow case reported.\n6. The walk costs about $40k of fees to 3.48x over six visible hours, plus arbitrage exposure, against up to roughly $830k kept via self-liquidation. What stops it is the hourly cap, fees, arbitrage, the Treasury's fall trigger and any keeper's honest refresh.\n\nRead in full: all eight scoped contracts, DeploymentConfig, ParameterizedVault, DeployMainnet and DeployPreflight, the body templates, the panel audit and the fix commit. CDPVault and Treasury were read for every price-consuming path only. Parameters, SwarmWorkOracle, Registry and Governed were not read.","treeHash":null,"usage":{"cachedInputTokens":4176658,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":93073,"runtime":"claude","turns":62,"wallClockMs":1416525}}],"verification":[]}