{"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":"f1346a93-e4d4-40da-9e2d-cf536b24e57a","kind":"audit","nodes":[{"acceptedSubmissionHash":"a600d2862312fefb6b4d157dfcdcbbbfdf8dec97e0e2095dff5f54ef6b558910","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":"6572c71642e6535f5e669ef3a8fe4e0a9d93491e80dac65cfcc4540ebbce170d","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":"5b558cf512f0f43527e77b8bfec686a4a631a2522fa1160237f59918235cef00","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":"6751d2e885c8661fb9709557b649a15c53d6eff24f7e3196df31b88db64a79c6","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":"0cd00a2dc1bdd8f4ae9257015b955006d62bd0b49f35a851a6f79cabbf00754a","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, src/SwarmWorkOracle.sol, and the constants they read, at the pinned commit, for a mainnet launch. Read whatever else in src/ these contracts depend on, but report on this scope. Three audit rounds and their fixes are already in (docs/AUDIT-*.md, newest docs/AUDIT-FINAL-2-2026-10-07.md and the fix commit after it); this panel audits the code as it will deploy, so a finding of an earlier round counts only if its fix regressed or left a gap.\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 (usedRequests), issuedAt freshness, panel floors, question binding over the window (expectedQuestionHash, WindowTooOld, WindowNotAdvancing). Can an attestation for a different question, chain, feed or window be accepted?\n2. The per-epoch deviation bound, as committed: every value within one lifetime of an epoch's start lies within the epoch's allowance of the ANCHOR; _allowanceNow is the cap while the value is fresh and through its first hour stale, then STALE_DEVIATION_MULTIPLE x cap plus an eighth of the cap per further whole hour (STALE_GROWTH_PERIOD), capped at MAX_ALLOWANCE_BPS; an epoch opened wider than the cap records its first value (_epochFirst, uint88 packed beside _updatedAt) and holds every later value in it to the cap around that value. State the largest move N attestations relayed within one lifetime can produce, the largest a single attestation can produce after H hours of silence, and the fastest sustained rate over a run of hours (the claim is the cap per hour after the first step); find any sequence faster, any way to keep a wide epoch open for a later value, any genuine gap that can never be followed, and check the uint32/uint88 packing, the unseeded case (_updatedAt 0), SwarmWorkOracle's override of _checkValue, and the one-day NHI feed under the same hourly schedule.\n3. OracleAsker: ask (keep-alive near stale; wideOpen = stale AND no live epoch AND allowance >= WIDE_ALLOWANCE_BPS; or an armed fall still present ARM_DELAY_BLOCKS later, never a rise), askPaid and askPaidMany (caller pays, in-flight feeds skipped uncharged), _request (a timed-out request keeps feedOf so its late delivery lands), onOracleResult (Intake-only, 200k-gas stipend, never reverts past the clearing, back-off only for the live request). Can anyone make the Treasury pay when the chain does not justify it, spend more than the daily budget, block updates for everyone, lose a paid answer, or feed a moved pool price into the trigger? The pool read is extsload of slot keccak256(IMD_POOL_ID, 6).\n4. The walk, costed: a ramp-and-hold of IMD's v4 pool (841 ETH / 207,881 IMD, 1% fee) that moves the feed the cap per hour against LINE $1M and mat 170: what it costs, what it earns, what stops it, in both directions (over-borrowing and forced liquidation).\n5. Price composition: UsdPriceFeed (IMD/ETH x Chainlink ETH/USD, 2-hour max age) and SharePriceFeed (exchange rate x IMD/USD per 1e18 raw units). Units, staleness propagation, what a reverting or non-standard asset leg does to every consumer.\n6. Feed lifetimes (price and spot 1 hour, NHI 1 day, tail() derived) and the vault acting on a value older than intended; SwarmRelay bundles (relay, relayMany, relayAndBark, relayAndBite) stranding funds or skipping a check; SwarmWorkOracle rights claimed twice, for someone else, or against a stale root.\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":"1f2a66f9623cec3d1680b6e8b5bf933e931e18d8974c5f1def84ddd23161a9a4","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"f1346a93-e4d4-40da-9e2d-cf536b24e57a","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":"52158","feedbackHash":"7fd25078f83f36ea82afbccc187dc547be2af776067cde9e9b814dfea88ce6c8","nodeKey":"audit_economics","submissionHash":"a600d2862312fefb6b4d157dfcdcbbbfdf8dec97e0e2095dff5f54ef6b558910","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52166","feedbackHash":"59bfc7ea82eac03ef7d784cfec50a5e25b7b2596991aea80babe550b4c1d8cc7","nodeKey":"audit_flow","submissionHash":"6572c71642e6535f5e669ef3a8fe4e0a9d93491e80dac65cfcc4540ebbce170d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52178","feedbackHash":"78e62307a990191a8b667ddbd4c3019604bfde9b5e7d9eef7145a2b9b2f92241","nodeKey":"audit_judge","submissionHash":"5b558cf512f0f43527e77b8bfec686a4a631a2522fa1160237f59918235cef00","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52174","feedbackHash":"60562ca1865abe91444575d76e11613ac88d15062e2e72edf4f507d0d0b85335","nodeKey":"audit_math","submissionHash":"6751d2e885c8661fb9709557b649a15c53d6eff24f7e3196df31b88db64a79c6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52167","feedbackHash":"a23148a5fb994964aa0c43698384f54f1dd5f76b603ca3c82fe6bfb54587653c","nodeKey":"audit_permissions","submissionHash":"0cd00a2dc1bdd8f4ae9257015b955006d62bd0b49f35a851a6f79cabbf00754a","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"8628b6afe12aeb70a166a2c44a52daaa700997cd082a4a5b805ca73323ff97ed","state":"completed","submissions":[{"artifacts":[],"attempt":2,"bundleHash":null,"device":"1ca477e8d9b58040","findings":[{"citation":"resolved","description":"The second-half review (docs/AUDIT-FINAL-2-2026-10-07.md, #1 medium) found the walk compounding at 1.4 an hour because the stale base applied one second past the lifetime; the fix (b73a05f) requires a whole STALE_GROWTH_PERIOD of staleness beyond maxAge. But staleness is measured from `_updatedAt`, which `_accept` sets to the attestation's SIGNED `issuedAt`, not to the block it was accepted in. `submitAttestation` admits an attestation whose issuedAt is up to maxAge old (`_tooOld(a.issuedAt)`), and `_requireQuestion` admits a window that closed up to maxAge/12 = 300 blocks ago; the live attestation e2c85027 was signed 130 s after its window closed and carries a one-day TTL (`validForSeconds: 86400` in deploy/mainnet/bodies/*.json), so a buyer can hold each purchased attestation about 57 minutes and relay it at the recency limit. Each acceptance then opens its epoch with `_updatedAt` already 57 minutes old; 62 minutes later the stored epoch has expired (`block.timestamp - _anchorAt >= maxAge`) and `periods = (now - issuedAt - 1h) / 1h = 1`, so `_epoch()` hands the next acceptance the stale base of 2 x cap (4000 bps at the shipped 2000). The claim in the SwarmFeed docstring (lines 27-30: 'a feed anyone keeps alive moves at most the cap per hour however it is driven'), in `_allowanceNow`'s NatSpec (lines 412-414) and in docs/PARAMETERS-2026-10-05.md ('3.48x in seven [steps], six hours') does not hold: from a fresh feed the walk is 1.2 then 1.4 per 62m10s, 3.29x at T0+4h07m and 4.61x at T0+5h09m, against the committed 2.07x at T0+4h and 2.49x at T0+5h. The attacker's exposure window to holders selling into the pump, the one cost the parameters doc relies on, is roughly halved, and the pool-fee model ($40k to 3.48x) is unchanged. The same holding trick applies to falls (forced liquidation direction) and to the one-day NHI feed (hold 23h59m, stale base at 25h after the previous acceptance rather than 25h after the previous signing). The suite's `test_relayingAnHourApartNeverEarnsTheStaleBase` signs every step at its relay block, which is why it passes. Reachable with the constants as committed: PriceFeed/SpotFeed maxAge 1 h, cap 2000, spans 300..1200 blocks, ATTESTATION_CHAIN_ID 1, SwarmRelay permissionless; the buyer needs only the x402 purchase ($4.25 each) and a mempool-free wait. Smallest fix: record the acceptance time and measure the silence from it, e.g. add `uint64 private _acceptedAt;` set to `uint64(block.timestamp)` in `_accept`, and in `_allowanceNow` compute `if (block.timestamp - _acceptedAt <= maxAge) return maxDeviationBps; uint256 periods = (block.timestamp - _acceptedAt - maxAge) / STALE_GROWTH_PERIOD;` (keep `_tooOld(_updatedAt)` for freshness). The slot the packed epoch lives in is full (64+8+64+32+88 bits), so this is one new slot: +22,100 gas on a first delivery, 146,876 -> ~169k, still inside the Intake's 200,000 stipend. Then regenerate the PARAMETERS table (every genuine recovery is measured from the acceptance). An alternative without a slot, tightening WindowTooOld to a few minutes, would also bound the delay but changes the delivery-latency policy.","line":415,"path":"src/SwarmFeed.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\n\n/// @dev A question-bound feed with the PriceFeed policy (one-hour life, 2,000 bps cap, 300..1200-block\n/// window, data chain 1) and an attester key the test holds. Relayer zero: SwarmRelay admits everyone\n/// anyway, so a permissionless feed is the same trust.\ncontract WalkFeed is SwarmFeed {\n    bytes private constant PREFIX = '{\"answerType\":\"uint256\",\"chainId\":1,\"question\":\"IMD/ETH\",\"v\":1,\"window\":{\"fromBlock\":';\n\n    constructor(address attester_) SwarmFeed(attester_, address(0), 1, 3, 1 hours, 2000) {}\n\n    function questionPolicy() internal pure override returns (bytes memory, uint64, uint64) {\n        return (PREFIX, 300, 1_200);\n    }\n}\n\n/// @notice SwarmFeed._allowanceNow measures the \"whole hour of silence\" that earns the stale base\n/// (2x the cap) from the SIGNED issuedAt, but the relayer chooses when to relay: the window-recency\n/// bound (WindowTooOld) lets an attestation land up to maxAge after its window closed, about 57\n/// minutes after it was signed. A buyer who holds each step for 57 minutes before relaying it opens\n/// every epoch on the stale base 62 minutes after the previous acceptance, and the walk compounds at\n/// 1.4 per 62 minutes rather than the committed 1.2 per hour.\ncontract DelayedRelayWalkTest is Test {\n    uint256 private constant KEY = 0xA11CE;\n    uint256 private constant V = 3.7e15; // wei of ETH per 1e18 IMD, about the live figure\n    uint256 private constant SIGN_LAG = 130; // the real attestation e2c85027: signed 2m10s after request\n\n    WalkFeed private feed;\n    uint256 private nonce;\n\n    function setUp() public {\n        vm.chainId(1); // the recency bound applies only on the data chain, which mainnet is\n        vm.roll(1_000_000);\n        vm.warp(12 * 1_000_000);\n        feed = new WalkFeed(vm.addr(KEY));\n    }\n\n    function test_holdingEachStepFiftySevenMinutesEarnsTheStaleBaseEveryHour() public {\n        // An honest value, signed and relayed now.\n        _relay(V, uint64(block.number), uint64(block.timestamp));\n        uint256 cap = feed.maxDeviationBps();\n        uint256 value = V;\n\n        // Step 1, one lifetime later: the cap, as committed. The attestation was bought 57 minutes ago\n        // (its window closed exactly 300 blocks ago, the last block WindowTooOld admits).\n        _advance(3600 + 12);\n        value = value * (10_000 + cap) / 10_000;\n        _relayHeld(value);\n\n        // Every later step: 62 minutes and 10 seconds after the previous ACCEPTANCE, the stored epoch has\n        // expired and the signed issuedAt of the held attestation is two whole hours old. PARAMETERS and\n        // the SwarmFeed docstring promise a feed anyone keeps alive moves at most the cap per hour; the\n        // allowance read here must therefore still be the cap, and a 1.4x step must be refused.\n        for (uint256 i = 2; i <= 5; ++i) {\n            _advance(3600 + SIGN_LAG);\n            (,, uint256 allowance) = feed.epoch();\n            assertEq(allowance, cap, \"62 minutes after the last acceptance the allowance is the cap, not 2x\");\n            uint256 wide = value * (10_000 + cap * feed.STALE_DEVIATION_MULTIPLE()) / 10_000;\n            _expectHeldRefused(wide);\n            value = value * (10_000 + cap) / 10_000;\n            _relayHeld(value);\n        }\n    }\n\n    // --- helpers -------------------------------------------------------------------------------\n\n    function _advance(uint256 seconds_) private {\n        vm.warp(block.timestamp + seconds_);\n        vm.roll(block.timestamp / 12);\n    }\n\n    /// @dev An attestation whose window closed 300 blocks ago (an hour; the recency limit) and that the\n    /// attester signed SIGN_LAG seconds after that: bought 57 minutes ago, relayed now.\n    function _relayHeld(uint256 figure) private {\n        uint64 toBlock = uint64(block.number - 300);\n        uint64 issuedAt = uint64(toBlock * 12 + SIGN_LAG);\n        _relay(figure, toBlock, issuedAt);\n    }\n\n    function _expectHeldRefused(uint256 figure) private {\n        uint64 toBlock = uint64(block.number - 300);\n        uint64 issuedAt = uint64(toBlock * 12 + SIGN_LAG);\n        (SwarmFeed.OracleAttestation memory a, bytes memory sig) = _signed(figure, toBlock, issuedAt);\n        vm.expectRevert(SwarmFeed.ExcessDeviation.selector);\n        feed.submitAttestation(a, sig);\n    }\n\n    function _relay(uint256 figure, uint64 toBlock, uint64 issuedAt) private {\n        (SwarmFeed.OracleAttestation memory a, bytes memory sig) = _signed(figure, toBlock, issuedAt);\n        feed.submitAttestation(a, sig);\n        (uint256 got, uint64 at) = feed.latestValue();\n        assertEq(got, figure);\n        assertEq(at, issuedAt);\n    }\n\n    function _signed(uint256 figure, uint64 toBlock, uint64 issuedAt)\n        private\n        returns (SwarmFeed.OracleAttestation memory a, bytes memory sig)\n    {\n        a.requestId = keccak256(abi.encode(\"request\", ++nonce));\n        a.chainId = 1;\n        a.fromBlock = toBlock - 600;\n        a.toBlock = toBlock;\n        a.questionHash = feed.expectedQuestionHash(a.fromBlock, a.toBlock);\n        a.answerType = 3;\n        a.answer = abi.encode(figure);\n        a.figure = figure;\n        a.blockHash = keccak256(abi.encode(\"block\", toBlock));\n        a.panelJobId = keccak256(abi.encode(\"panel\", nonce));\n        a.panelSize = 60;\n        a.quorum = 20;\n        a.agreed = 20;\n        a.issuedAt = issuedAt;\n        a.expiresAt = issuedAt + 1 days; // the live attestation's TTL\n        bytes32 structHash = keccak256(\n            bytes.concat(\n                abi.encode(\n                    feed.ATTESTATION_TYPEHASH(),\n                    a.requestId,\n                    a.chainId,\n                    a.questionHash,\n                    a.answerType,\n                    keccak256(a.answer),\n                    a.figure,\n                    a.fromBlock\n                ),\n                abi.encode(\n                    a.toBlock, a.blockHash, a.panelJobId, a.panelSize, a.quorum, a.agreed, a.issuedAt, a.expiresAt\n                )\n            )\n        );\n        bytes32 digest = keccak256(abi.encodePacked(\"\\x19\\x01\", feed.DOMAIN_SEPARATOR(), structHash));\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(KEY, digest);\n        sig = abi.encodePacked(r, s, v);\n    }\n}","reproduction":"Question-bound feed with the PriceFeed policy (maxAge 1 h, cap 2000, span 300..1200, data chain 1, relayer zero which is the same trust as SwarmRelay), chain id 1, 12-second blocks. T0: honest value V = 3.7e15 relayed, issuedAt = T0. T0+1h+12s: relay 1.2V with toBlock = block.number-300 (the last block WindowTooOld admits) and issuedAt = toBlock*12 + 130 (signed 2m10s after the window closed, bought 57 minutes before relay): accepted (allowance 2000). T0+1h+12s+3730s (62m10s after that acceptance): `feed.epoch()` EXPECTED allowanceBps 2000 (cap per hour, SwarmFeed docstring 27-30 and PARAMETERS 'The walk, at the committed constants'); ACTUAL 4000. A held attestation for 1.2V x 1.4 = 1.68V is EXPECTED refused ExcessDeviation; ACTUAL accepted. Repeating every 3730 s: 2.352V, 3.293V (T0+4h07m), 4.61V (T0+5h09m). test/scratch/DelayedRelayWalk.t.sol fails on this code at the first epoch() assertion (4000 != 2000) and passes against a copy of SwarmFeed with an `_acceptedAt` slot measured as in the fix above.","severity":"medium","snippet":"        uint256 periods = (block.timestamp - _updatedAt - maxAge) / STALE_GROWTH_PERIOD;","title":"SwarmFeed._allowanceNow measures the hour of silence that earns the stale base from the signed issuedAt, which the relayer may hold back up to the window-recency limit (~57 min), so a held-and-relayed"},{"citation":"resolved","description":"The back-off was written for a Treasury-bought answer the feed refused ('A refused answer must not be bought again ten minutes later'), and the NatSpec says 'askPaid is unaffected: its caller pays'. But `live` is true for ANY request still in the feed's in-flight slot, including one bought with `askPaid`/`askPaidMany`, and `lastAsk` is the gate `ask` applies to Treasury money (`TooSoon`). The plane publishes the signed attestation before the Intake's callback transaction lands, and SwarmRelay admits everyone, so the buyer (or anyone watching the mempool) can relay the same bytes first; the callback's `relay` then reverts `ReplayedAttestation`, the catch sets `lastAsk = now + 2h - 10m`, and `ask` reverts `TooSoon` for two hours whatever the pool does. Repeated every two hours this costs the attacker 0.5 IMD (~$5) per cycle, about $60 a day, and removes the Treasury as a buyer: an armed 5% fall is not bought (collateral stays over-valued until the hour-old value goes stale and the vault halts), the NHI keep-alive at 18 h is not bought, and a wide-open silent feed is not refreshed. The manual fallbacks remain (askPaid by the imd-keeper, hand relay), which is why this is low rather than medium: nothing is mispriced, the protocol only loses its own automatic refresh and relies on a keeper key the design says it should not need. Smallest fix: remember who paid and back off only on the Treasury's own refusal, e.g. add `bool treasuryPaid;` to `Feed` (the struct's second slot has room), set it in `ask` and clear it in `askPaid`/`askPaidMany` when they take the slot, and change the catch to `if (live && f.treasuryPaid) f.lastAsk = ...`. Alternatively back off only on refusals that say something about the feed's state (ExcessDeviation) and not on `ReplayedAttestation`/`WindowNotAdvancing`, which only say the answer already landed.","line":284,"path":"src/OracleAsker.sol","reproduction":"Fixture as in test/OracleAsker.t.sol (MockIntake at INTAKE, SwarmRelay at ATTESTATION_RELAYER, MockPoolManager at POOL_MANAGER, price feed maxAge 1 h cap 2000 seeded at 0.001 ETH, asker holding 15 IMD, tracksPool true, keepAlive false). ATTACKER approves 0.5 IMD and calls askPaid(priceFeed, body, 0.5e18) -> R. ATTACKER calls SwarmRelay.relay(priceFeed, a, sig) with the attestation the plane signed for R (figure 0.001 ETH, issuedAt now): accepted. The Intake completes R with the same bytes: Delivered(relayed=false), and `feeds(priceFeed).lastAsk == now + 2h - 10min`. Ten minutes later the pool slot0 is set to a 10% fall; `arm(priceFeed)` succeeds; five blocks later `ask(priceFeed, body)` EXPECTED to pay the Intake 0.5 IMD (an armed fall still present, the Treasury's documented trigger); ACTUAL reverts TooSoon(lastAsk + ASK_MIN_INTERVAL), a timestamp two hours after the refusal; the asker's balance is unchanged. Scratch test test/scratch/PaidRefusalBacksOffTreasury.t.sol demonstrates the sequence (passes on this code).","severity":"low","snippet":"            if (live) f.lastAsk = uint64(block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL);","title":"OracleAsker.onOracleResult backs the Treasury off for ASK_TIMEOUT after a refused delivery of a CALLER-paid request, so a $5 askPaid whose public answer is relayed by hand first disables the Treasury'"},{"citation":"resolved","description":"Since cc4103f the deviation guard is per epoch: every value accepted within maxAge of an epoch's start must lie within the epoch's allowance of the ANCHOR (the value the feed held when the epoch opened), and the allowance is the cap only for an epoch opened on a fresh value (contract docstring lines 22-33, `_checkValue`, `_epoch`, `_allowanceNow`). The @param line still states the pre-epoch rule. A reader choosing a cap from this line would expect 'at most cap per attestation' and get 'at most the epoch allowance per lifetime, up to 100x after long silence'. No code change; reword the @param to 'the per-epoch allowance of the anchor when the epoch opens on a fresh value; wider on a stale one (see _allowanceNow)'.","line":156,"path":"src/SwarmFeed.sol","reproduction":"Feed with maxAge 1 h and maxDeviationBps 2000 seeded V at T0. At T0+30m accept 1.2V (within the cap of the anchor V). At T0+40m submit 1.44V: expected per the @param text (20% from the LAST accepted value 1.2V) accepted; actual ExcessDeviation, because the epoch's anchor is V and 1.44V is 44% from it. Conversely at T0+2h+1s (two whole hours past issuedAt) a value 1.4V is accepted although it is 40% from the last accepted value.","severity":"info","snippet":"    /// @param maxDeviationBps_ Maximum change from the last accepted value, from 0 to 10,000 bps.","title":"SwarmFeed's constructor NatSpec describes maxDeviationBps_ as a bound on the change from the LAST accepted value; the shipped bound is per epoch, measured from the epoch's anchor"},{"citation":"resolved","description":"SwarmFeed.ATTESTATION_TYPEHASH (src/SwarmFeed.sol:117-119) covers `...bytes32 panelJobId,uint16 panelSize,uint16 quorum,uint16 agreed,uint64 issuedAt,uint64 expiresAt` and DOMAIN_SEPARATOR (lines 166-174) uses `keccak256(\"2\")`. ABI.md, the integrator-facing reference, still documents the v1 shape: a 12-field tuple with `panelJobId` followed directly by `issuedAt`, and domain version `1`. An integrator (a relayer or a keeper) encoding `submitAttestation` from this page produces calldata that does not decode, or a digest the feed rejects with InvalidSignature. It also omits the panel floors (MIN_PANEL_SIZE 25, MIN_AGREED 15) that the v2 fields enable. Documentation only; regenerate the paragraph from the struct and the domain in SwarmFeed.sol.","line":84,"path":"docs/ABI.md","reproduction":"Build the tuple as ABI.md line 84 lists it, (bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint64,uint64), sign it under EIP712Domain('IdentityMD Oracle','1',chainid,feed) with the attester key, and call `submitAttestation` on any shipped feed: expected per the doc, accepted; actual, the call reverts in ABI decoding (the struct has 15 members) and, with the three fields added but version '1', reverts InvalidSignature because DOMAIN_SEPARATOR hashes version '2'.","severity":"info","snippet":"`submitAttestation` takes an `OracleAttestation` tuple in this exact order: `(bytes32 requestId, uint256 chainId, bytes32 questionHash, uint8 answerType, bytes answer, uint256 figure, uint64 fromBlock, uint64 toBlock, bytes32 blockHash, bytes32 panelJobId, uint64 issuedAt, uint64 expiresAt)`, followed by a 65-byte signature. The EIP-712 domain is `IdentityMD Oracle`, version `1`, with the deployment chain ID and the receiving feed's address, computed once in its constructor. Payload chainId and answerType must equal `attestationChainId()` and `attestationAnswerType()`; the payload data chain may differ from the consumer chain (for example, mainnet data consumed on Sepolia). requestId is consumed once per feed; issuedAt cannot be in the future, exceed expiresAt, precede the last accepted update, or be older than maxAge. Delivery after expiresAt is rejected. The feed publishes figure and uses signed issuedAt for freshness, then discards any unfinished reporter round.","title":"docs/ABI.md describes the attestation as a 12-field tuple under EIP-712 domain version '1'; SwarmFeed signs 15 fields (panelSize, quorum, agreed added) under version '2'"}],"hash":"0cd00a2dc1bdd8f4ae9257015b955006d62bd0b49f35a851a6f79cabbf00754a","nodeId":"144eed84-3336-4f73-9459-6161eb580fa5","outcome":"completed","summary":"The embedded proof fails on the current code as required; scratch files are removed and only `.imd-findings.json` remains untracked. Here is the report.\n\n## Findings (in `.imd-findings.json`)\n\n**1. MEDIUM — `SwarmFeed._allowanceNow` (src/SwarmFeed.sol:415): the hour of silence that earns the stale base is measured from the signed `issuedAt`, which the relayer may hold back ~57 minutes.** The second-half review's medium (#1) fix requires a whole hour of staleness past `maxAge`, but `_updatedAt` is the attestation's `issuedAt`, not the acceptance block. `submitAttestation` admits `issuedAt` up to `maxAge` old and `_requireQuestion` admits a window up to 300 blocks old; the live attestation was signed 130 s after its window and the bodies carry a 1-day TTL. So a buyer holds each step 57 minutes and relays it at the recency limit; 62 minutes after that acceptance the epoch has expired and `periods == 1`, so the next step gets 2× cap. Walk from a fresh feed: 1.2, then 1.4 per 62m10s → 3.29× at T0+4h07, 4.61× at T0+5h09, against the committed 2.07× / 2.49×. The claim at lines 27-30, 412-414 and in PARAMETERS (\"3.48× in seven steps, six hours\") does not hold. Reachable with committed constants. Proof: a self-contained test fails at `epoch()` returning 4000 and passes against a copy with an `_acceptedAt` slot used for the silence (smallest fix; +22.1k gas on a first delivery, 146,876 → ~169k, inside the 200k stipend).\n\n**2. LOW — `OracleAsker.onOracleResult` (src/OracleAsker.sol:284) backs the Treasury off after a refused *caller-paid* delivery.** `live` is true for an `askPaid` request too. Relay the plane's public answer by hand before the callback → `ReplayedAttestation` → `lastAsk = now+2h−10m` → `ask` reverts `TooSoon` for two hours even with an armed 5% fall present. $5 per two hours disables the Treasury's fall trigger, NHI keep-alive and wide refresh; manual fallbacks remain. Fix: back off only on the Treasury's own refusal (a `treasuryPaid` flag in `Feed`).\n\n**3. INFO** — `@param maxDeviationBps_` (SwarmFeed.sol:156) still describes the pre-epoch \"from the last accepted value\" rule.\n**4. INFO** — docs/ABI.md:84 documents a 12-field tuple under domain version \"1\"; the code signs 15 fields under \"2\".\n\n## Answers to the numbered questions\n\n1. **Attestation acceptance** — sound. Domain binds chainid + feed address (no cross-feed/cross-chain replay), `usedRequests` per feed, low-s/v checks, `issuedAt ≤ now`, `≥ _updatedAt`, within `maxAge`; panel floors 25/15 (`quorum` is signed but unchecked, by design); window spliced into the pinned RFC-8785 prefix (`window` sorts last; `fromBlock` < `toBlock`), span bounds, advancing `toBlock`, recency bound on chain 1. A different question/chain/feed/window cannot be accepted. One note: the max span (1200) lets a buyer substitute a 4-hour median for the pinned 2-hour body; the spot/primary skew guard contains it.\n2. **Epoch bound** — within one lifetime, N attestations move at most the epoch allowance (cap if opened fresh). A single attestation after H hours of silence (from `issuedAt`): cap for H < 2, 40% at 2, 45% at 4, 60% at 10, 100% at 26, 100× ceiling; a fall is always followable within 27 h, a rise above 100× never (only the NHI feed near zero could hit this, practically unreachable since r≈1 keeps NHI ≥ 0.3). Sustained rate: committed cap/hour; **actual 2×cap per 62 min via held relay (finding 1)**. `_epochFirst` cannot be kept open (anchor bound ∧ first±cap); uint32/uint88 packing exact; unseeded case reads (0, now, 1e6) and `_accept` opens at the cap; work-oracle override keeps only the zero check; NHI widens hourly as documented.\n3. **OracleAsker** — `wideOpen`, `nearStale`, arm/ask, `askPaid(Many)`, late deliveries and the slot-6 `extsload` (v4 `POOLS_SLOT = 6`, sqrtPriceX96 in the low 160 bits, inversion correct) all check out; the Treasury cannot spend over budget (top-up to budget); a paid answer is not lost. The only gap is finding 2.\n4. **The walk, costed** — pump to M costs ","treeHash":null,"usage":{"cachedInputTokens":6861898,"inputTokens":95,"model":"claude-fable-5-1","outputTokens":79980,"runtime":"claude","turns":50,"wallClockMs":1192756}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"61794f1d37e04d2f","findings":[],"hash":"0ce8946b9fc6b3627f3a62a54e2aa08fd0a000cc6a8981e20ff05fb373a3bb54","nodeId":"cb20d9d9-477a-47d5-bcb1-12ffb844aafe","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":3,"wallClockMs":374380}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5c0fdae3b22cd896","findings":[],"hash":"12bfb2c7f32346b0bf24ba5d9aa637a2baca2ee08b078b7504da39e8607a9c78","nodeId":"5c6c409e-09cf-4fe9-b909-d64d6436491a","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":3,"wallClockMs":480288}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f768e94767a9dde3","findings":[{"citation":"resolved","description":"Merged from four specialist reports (audit_permissions, audit_flow, audit_economics, audit_math), all reproduced. The b73a05f fix for the second-half review's medium requires a whole STALE_GROWTH_PERIOD of staleness past maxAge before `_allowanceNow` returns STALE_DEVIATION_MULTIPLE x cap, but it measures that staleness from `_updatedAt`, which `_accept` (line 463) sets to the attestation's SIGNED `issuedAt`, while the epoch itself is dated from the relay block (`_anchorAt = block.timestamp`, line 455). `submitAttestation` admits an issuedAt up to maxAge old (line 233, `_tooOld(a.issuedAt)` is false at exactly one lifetime) and `_requireQuestion` admits a window that closed up to maxAge/12 = 300 blocks ago (line 309); the shipped bodies sign `validForSeconds: 86400`. So a buyer who holds each purchased attestation ~55-57 minutes before relaying it through the permissionless SwarmRelay opens every epoch with `_updatedAt` already ~an hour old. One lifetime plus a few minutes after that relay the stored epoch has expired AND `block.timestamp - _updatedAt - maxAge >= 1 hour`, so `periods == 1` and the next acceptance gets 4,000 bps at the shipped 2,000 cap. The walk then compounds at 1.4 per ~65 minutes instead of 1.2 per hour: from a fresh feed 1.2, 1.68, 2.35, 3.29x (T0+4h15m), 4.61x (T0+5h20m) against the committed 2.07x at 4h and 2.49x at 5h; 3.48x (the level docs/PARAMETERS-2026-10-05.md costs at ~$39k of pool fees against ~$500k over-borrowing at LINE $1M / mat 170) is reached in 4-5 hours of holding the pool instead of 6-7. The same hold works in the fall direction (forced liquidation) and on the one-day NHI feed (hold 23h59m; a value held that long reads 4,000 + 250 x 22 = 9,500 bps on the next step instead of the cap). It also shifts OracleAsker.wideOpen and the H-hours table one hour early relative to the actual relay. NatSpec claims the code does not have: SwarmFeed.sol lines 27-30 ('The stale base is earned by silence, never by timing ... a feed anyone keeps alive moves at most the cap per hour however it is driven'), lines 47-51 ('a run of steps compounds at no more than the cap per hour after the first'), and lines 412-414 inside `_allowanceNow`; docs/PARAMETERS-2026-10-05.md 'The walk, at the committed constants'. The regression test `test_relayingAnHourApartNeverEarnsTheStaleBase` only relays with issuedAt == block.timestamp, which is why it passes. Reachable with the constants as committed (maxAge 1h, cap 2000, spans 300..1200 / 150..1200, ATTESTATION_CHAIN_ID 1, SwarmRelay permissionless); needs no privileged role, only off-chain purchase of the attestations and a wait before relaying, which the runbook and NatSpec describe as the normal fallback. SMALLEST FIX (verified by the judge: all three specialist proofs pass and test/SwarmFeed.t.sol, OracleAsker*.t.sol, QuestionBinding, RelayBundling and SwarmRelay suites still pass): measure the silence from the later of the signed time and the epoch's open, in `_allowanceNow`: `uint256 since = _anchorAt > _updatedAt ? _anchorAt : _updatedAt; if (block.timestamp - since <= maxAge) return maxDeviationBps; uint256 periods = (block.timestamp - since - maxAge) / STALE_GROWTH_PERIOD;` — both fields are in the slot `_accept` already writes, so no gas change for the Intake's 200k stipend. (A mid-epoch late relay still cannot help the attacker: inside an epoch every value is bounded by the anchor's allowance, so the residual under-measurement from `_anchorAt` yields at most 40% per two hours, no faster than the cap per hour.) An exact alternative is a new `_acceptedAt` slot written in `_accept` (+22,100 gas on a first delivery, still inside the stipend). Then regenerate the PARAMETERS walk table and reword the three NatSpec passages.","line":415,"path":"src/SwarmFeed.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\n\n/// @dev The PriceFeed's launch policy (1-hour lifetime, 2,000 bps cap) with an attester whose key this\n/// test holds and no pinned question, so only the epoch bound is exercised.\ncontract HeldFeed is SwarmFeed {\n    constructor(address attester_, address relayer_) SwarmFeed(attester_, relayer_, 1, 3, 1 hours, 2_000) {}\n}\n\n/// @notice FINDING: the stale base is earned by the age of the signed `issuedAt`, not by silence of the\n/// feed. `_allowanceNow` measures \"whole periods stale\" from `_updatedAt`, which `_accept` sets to the\n/// attestation's signed issue time, while the epoch is measured from the block the value was relayed in.\n/// `submitAttestation` accepts an issuedAt up to one lifetime old, so a buyer who holds every answer for\n/// most of an hour before relaying it opens every epoch on a value the feed believes has been stale for\n/// a whole hour, and takes STALE_DEVIATION_MULTIPLE x the cap (40%) at every step, one step an hour.\n///\n/// The NatSpec (SwarmFeed.sol:27-30, 47-49, 410-414) says the stale base is \"earned by silence, never by\n/// timing\" and that \"a feed anyone keeps alive moves at most the cap per hour however it is driven\".\n/// This test keeps the feed alive with one relay an hour and moves it 1.2 x 1.4^3 = 3.29x in four hours\n/// where the cap per hour allows 1.2^4 = 2.07x. It fails on the committed code and passes once the\n/// allowance's staleness is measured from the later of the signed time and the epoch's open (the relay).\ncontract StaleBaseByHeldAnswerTest is Test {\n    uint256 private constant ATTESTER_KEY = 0xA11CE;\n    uint256 private constant V = 3_550_000 gwei; // ~0.00355 ETH per IMD, the live figure\n    uint256 private constant HOLD = 1 hours - 5 minutes; // the panel answers minutes after the window closes\n    HeldFeed private feed;\n    uint256 private nonce;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.warp(10 days);\n        vm.roll(1_000_000);\n        feed = new HeldFeed(vm.addr(ATTESTER_KEY), address(this));\n        // Honest seed, issued now.\n        _submit(V, uint64(block.timestamp));\n    }\n\n    /// @dev One relay an hour (plus the five minutes the hold leaves), every answer held HOLD before it\n    /// is relayed. Expected under the documented bound: each step is at most the cap (20%). Actual: the\n    /// second step onwards opens on the 40% stale base.\n    function test_aFeedKeptAliveHourlyMovesAtMostTheCapPerHour() public {\n        uint256 cap = feed.maxDeviationBps();\n        uint256 value = V;\n        // Step 1, an hour after the seed: the epoch has run its lifetime, the allowance is the cap.\n        _advance(1 hours);\n        value = value * (10_000 + cap) / 10_000;\n        _submit(value, uint64(block.timestamp - HOLD));\n        // Steps 2-4: each relayed one hour and five minutes after the previous relay, so the feed has\n        // never been silent for a whole hour past its lifetime, yet its allowance reads 2x the cap.\n        for (uint256 i; i < 3; ++i) {\n            _advance(1 hours + 5 minutes);\n            (,, uint256 allowance) = feed.epoch();\n            assertLe(allowance, cap, \"an hourly-relayed feed must not open its epoch on the stale base\");\n            // What the code actually lets through: a 40% step.\n            uint256 attempt = value * (10_000 + 2 * cap) / 10_000;\n            bool accepted = _try(attempt, uint64(block.timestamp - HOLD));\n            assertFalse(accepted, \"a 40% step was accepted an hour after the last relay\");\n            // Keep the feed alive either way: the honest cap step is what an hourly relayer sends.\n            value = accepted ? attempt : value * (10_000 + cap) / 10_000;\n            if (!accepted) _submit(value, uint64(block.timestamp - HOLD));\n        }\n    }\n\n    /// @dev The same walk expressed as a bound on the end value: four hourly relays may multiply the\n    /// feed by at most 1.2^4 = 2.0736 with a 2,000 bps cap.\n    function test_fourHourlyRelaysMoveTheFeedAtMostTheCapToTheFourth() public {\n        uint256 value = V;\n        _advance(1 hours);\n        value = value * 12 / 10;\n        _submit(value, uint64(block.timestamp - HOLD));\n        for (uint256 i; i < 3; ++i) {\n            _advance(1 hours + 5 minutes);\n            uint256 attempt = value * 14 / 10;\n            if (_try(attempt, uint64(block.timestamp - HOLD))) value = attempt;\n        }\n        (uint256 got,) = feed.latestValue();\n        assertLe(got, V * 20_736 / 10_000 + 1, \"four hourly relays walked the feed past 1.2^4\");\n    }\n\n    function _advance(uint256 secs) private {\n        vm.warp(block.timestamp + secs);\n        vm.roll(block.number + secs / 12);\n    }\n\n    function _submit(uint256 figure, uint64 issuedAt) private {\n        SwarmFeed.OracleAttestation memory a = _attestation(figure, issuedAt);\n        feed.submitAttestation(a, _sign(a));\n    }\n\n    function _try(uint256 figure, uint64 issuedAt) private returns (bool ok) {\n        SwarmFeed.OracleAttestation memory a = _attestation(figure, issuedAt);\n        bytes memory sig = _sign(a);\n        try feed.submitAttestation(a, sig) {\n            ok = true;\n        } catch (bytes memory reason) {\n            assertEq(bytes4(reason), SwarmFeed.ExcessDeviation.selector, \"refused for another reason\");\n        }\n    }\n\n    function _attestation(uint256 figure, uint64 issuedAt) private returns (SwarmFeed.OracleAttestation memory a) {\n        a.requestId = keccak256(abi.encode(\"request\", ++nonce));\n        a.chainId = 1;\n        a.questionHash = keccak256(\"question\");\n        a.answerType = 3;\n        a.answer = abi.encode(figure);\n        a.figure = figure;\n        a.fromBlock = uint64(block.number - 300);\n        a.toBlock = uint64(block.number);\n        a.blockHash = keccak256(\"block\");\n        a.panelJobId = keccak256(\"panel\");\n        a.panelSize = 60;\n        a.quorum = 20;\n        a.agreed = 40;\n        a.issuedAt = issuedAt;\n        a.expiresAt = uint64(block.timestamp + 1 days);\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                    feed.ATTESTATION_TYPEHASH(),\n                    a.requestId,\n                    a.chainId,\n                    a.questionHash,\n                    a.answerType,\n                    keccak256(a.answer),\n                    a.figure,\n                    a.fromBlock\n                ),\n                abi.encode(\n                    a.toBlock, a.blockHash, a.panelJobId, a.panelSize, a.quorum, a.agreed, a.issuedAt, a.expiresAt\n                )\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) =\n            vm.sign(ATTESTER_KEY, keccak256(abi.encodePacked(\"\\x19\\x01\", feed.DOMAIN_SEPARATOR(), body)));\n        return abi.encodePacked(r, s, v);\n    }\n}","reproduction":"HeldFeed (SwarmFeed with maxAge 1 hours, cap 2_000, relayer = test, attester key held by the test; the same policy as PriceFeed), chain id 1. T0: submit V = 3.55e15 with issuedAt = T0. T0+1h: submit 1.2V with issuedAt = T0+1h-55min (accepted: issuedAt >= _updatedAt and not too old; epoch opens, allowance 2000). T0+2h05m (65 minutes after that relay): EXPECTED per NatSpec 27-30 feed.epoch() allowanceBps == 2000 and a figure 1.68V (a 40% step) refused with ExcessDeviation; ACTUAL allowanceBps == 4000 and 1.68V is accepted (issuedAt = now - 55min). Repeating every 65 minutes: 2.352V at T0+3h10m, 3.293V at T0+4h15m, latestValue 11,689,440,000,000,000 against the cap-per-hour ceiling 1.2^4 V = 7,361,280,000,000,000. The attached test (test/scratch/Proof_bf1bb6b60b3d.t.sol, from the audit_math specialist) fails on the committed code with '4000 > 2000' and '11689440000000000 > 7361280000000001' and passes with the `since = max(_anchorAt, _updatedAt)` patch above. The same reproduction with a question-bound feed (span 300..1200, toBlock = head-300, issuedAt = toBlock*12+130) in test/scratch/Proof_95b1f17cb9d0.t.sol also fails on this code and passes with the fix.","severity":"medium","snippet":"        uint256 periods = (block.timestamp - _updatedAt - maxAge) / STALE_GROWTH_PERIOD;","title":"SwarmFeed._allowanceNow measures the hour of silence that earns the stale base from the signed issuedAt, which a relayer may hold back up to maxAge, so a held-and-relayed walk compounds at 2x the cap "},{"citation":"resolved","description":"Reproduced from the audit_permissions specialist. The back-off was written for a Treasury-bought answer the feed refused ('A refused answer must not be bought again ten minutes later') and the catch comment says 'askPaid is unaffected: its caller pays'. But `live` (line 265) is true for ANY request still in the feed's in-flight slot, including one bought with `askPaid`/`askPaidMany`, and `lastAsk` is the gate `ask` applies to Treasury money (`TooSoon`, lines 156-158). The plane publishes the signed attestation before the Intake's callback lands and SwarmRelay admits everyone, so the buyer (or anyone watching) relays the same bytes first; the callback's `relay` then reverts `ReplayedAttestation`, the catch writes `lastAsk = now + 2h - 10m`, and `ask` reverts `TooSoon` for two hours whatever the pool does. Repeated every two hours this costs 0.5 IMD (~$5) per cycle, about $60 a day, and removes the Treasury as a buyer: an armed 5% fall is not bought (collateral stays over-valued until the hour-old value goes stale and the vault pauses), the NHI keep-alive at 18h is not bought, a wide-open silent feed is not refreshed. Manual fallbacks (askPaid by a keeper, hand relay) remain and nothing is mispriced, so low. The NatSpec claim the code does not have: lines 281-282 'askPaid is unaffected: its caller pays' is true of the gate on askPaid but not of what a paid request's refusal does to the Treasury's own gate. Smallest fix: remember who paid and back off only on the Treasury's own refusal: add `bool treasuryPaid;` to `Feed` (the struct's second slot has room), set it in `ask` and clear it in `askPaid`/`askPaidMany` where they take the slot, and change the catch to `if (live && f.treasuryPaid) f.lastAsk = ...`. Alternatively do not back off on `ReplayedAttestation`/`WindowNotAdvancing`, which only say the answer already landed.","line":284,"path":"src/OracleAsker.sol","reproduction":"test/scratch/PaidRefusalBacksOffTreasury.t.sol (passes on this code: it demonstrates the state). Fixture as test/OracleAsker.t.sol: MockIntake at INTAKE, SwarmRelay at ATTESTATION_RELAYER, MockPoolManager at POOL_MANAGER, ConfigurableSwarmFeed(maxAge 1h, cap 2000) seeded at 0.001 ETH, asker holding 15 IMD, tracksPool true, keepAlive false. ATTACKER approves 0.5 IMD and calls askPaid(priceFeed, body, 0.5e18) -> R. ATTACKER calls SwarmRelay.relay(priceFeed, a, sig) with the attestation signed for R (figure 0.001 ETH, issuedAt now): accepted. The Intake completes R with the same bytes: Delivered(relayed=false) and `feeds(priceFeed).lastAsk == now + 2h - 10min` (asserted). Ten minutes later the pool slot0 is set to a 10% fall, `arm(priceFeed)` succeeds, five blocks later `ask(priceFeed, body)`: EXPECTED the Treasury pays the Intake 0.5 IMD (an armed fall still present, its documented trigger); ACTUAL reverts TooSoon(lastAsk + ASK_MIN_INTERVAL) and the asker's balance is unchanged (asserted).","severity":"low","snippet":"            if (live) f.lastAsk = uint64(block.timestamp + ASK_TIMEOUT - ASK_MIN_INTERVAL);","title":"OracleAsker.onOracleResult backs the Treasury off for ASK_TIMEOUT after a refused delivery of a CALLER-paid request, so a ~$5 askPaid whose public answer is hand-relayed first disables Treasury-paid a"},{"citation":"resolved","description":"Reproduced from the audit_flow specialist. `_checkValue` applies no deviation bound while `_hasValue` is false (line 372), and the NatSpec here and at lines 221-222 ('a nonzero relayer covers the unseeded first value and stale re-anchors') says the first value is the deployer's. On mainnet the pinned relayer is SwarmRelay, which forwards for anyone (constructor comment, lines 181-193), the question bodies are public (deploy/mainnet/bodies, `{{FEED}}` filled with the planned CREATE2 address) and script/DeployMainnet.s.sol deploys the feeds without seeding them: docs/MAINNET-RUNBOOK.md section 7.1 buys and relays the first attestations as a later operational act, and `verify()` checks relayer/attester/policy but never the anchor against the pool. So the first accepted value of PriceFeed, SpotFeed and NhiFeed is set by whoever relays first, and ParameterizedVault (permissionless from its constructor; 'only then open deposits' is an announcement, not an on-chain gate) prices every position off that anchor. A first value at 2x market is a signed, honest answer to the pinned question if the pool is held at 2x for 7 of the window's 13 samples (the Q4 ramp-and-hold, without a prior walk or any silence). The deployer's honest attestation (market, 0.5x the anchor) is refused ExcessDeviation for the first epoch's hour and afterwards until `_allowanceNow` reaches 5,000 bps, which is periods >= 5, six hours after the attacker's issuedAt (five with the hold of the medium finding). Reachable with the constants as committed; it is a race the attacker must win in the minutes between deployment and the deployer's relay while holding a pumped pool through a window, so low. NatSpec claims the code does not have: lines 356-357 and 221-222. Smallest fix, either: have DeployMainnet buy the three attestations for the planned addresses before the broadcast and relay them in the same transaction as the deployment (the attester signs for the consumer address in the body, which the plan already fixes), so no block exists in which the feeds are unseeded; or make the first anchor checkable before deposits are announced, e.g. `verify()` requires `|priceFeed.latestValue() - asker.poolPrice()| <= cap`. Reword 356-357 and 221-222 to say the first value is bounded by nothing on chain and belongs to whoever relays first.","line":356,"path":"src/SwarmFeed.sol","reproduction":"test/scratch/FirstValueRace.t.sol (passes on this code: it demonstrates the state). RaceFeed(maxAge 1h, cap 2000, relayer = a fresh SwarmRelay), no value. STRANGER calls SwarmRelay.relay(feed, a, sig) with a valid attester-signed attestation, figure 2V (V = 3.55e15): EXPECTED per the NatSpec the first value is the deployer's; ACTUAL accepted, epoch() reports anchor 2V, allowance 2000. DEPLOYER relays an honest attestation with figure V: reverts ExcessDeviation (|V - 2V| = V > 0.2 x 2V). At T+5h59m the allowance is 4750 and V is still refused; at T+6h00m01s the allowance is 5000 and V is finally accepted.","severity":"low","snippet":"    /// stale (`_allowanceNow`). The first value ever has no\n    /// bound: it is the one we buy and check at deployment, and it anchors the first epoch.","title":"SwarmFeed: the unbounded first value is 'the one we buy and check at deployment' only by convention; with SwarmRelay as the pinned relayer whoever relays first anchors a freshly deployed feed, and the"},{"citation":"resolved","description":"Reproduced from the audit_economics specialist (Q5: what a reverting or non-standard leg does to every consumer). UsdPriceFeed._ethUsd maps an aggregator that reverts or answers malformed to (0, 0, 0), so UsdPriceFeed.latestValue returns (0, 0) and SharePriceFeed.latestValue returns (0, 0) too (also when sIMD's convertToAssets reverts); ParameterizedVault._priceOrZero then returns 0. Price actions are refused (StaleFeed), the documented safe direction, but `lock`, `lockIMD` and `wipe` are deliberately ungated and each calls `_resecure(position, _priceOrZero())` (CDPVault.sol lines 378, 403, 1151); `_secured` (this line) returns 0 for a zero price, so the position's whole term leaves `securedCollateral`, and `_clampLag` (lines 849-850) treats the drop as a real decrease and clamps `laggedSecured` down with it. Nothing re-prices the term when the leg recovers: only that borrower's next lock/free/draw/wipe does, and the restored amount then counts only as it warms up over BACKING_WARMUP (a day). `_backingPerUnit` (line 677) therefore reads 0 for a single-borrower vault (or is depressed by that borrower's share in general) after the leg is back, and `cash` computes payoutScale = 0 and reverts ZeroAmount (line 634), or pays below par, while every position is as collateralised as before; the redemption channel, the peg defence, is shut or underpaying for the rest of that day because of an oracle outage that is otherwise over. A merely STALE Chainlink answer does not do this (latestValue still returns the old figure); it needs the aggregator to revert or return malformed words, or sIMD's convertToAssets to revert, both cases the code explicitly handles by reading zero. Nothing is stolen and redeemers can protect themselves with minGemOut, so low. NatSpec at lines 813-815 ('an unpriced feed counts the position for nothing') describes the write but not that it persists past recovery and feeds the lag clamp. Smallest fix: in `_resecure`, when `price == 0` leave `position.secured` and `securedCollateral` untouched (skip the re-pricing) instead of writing a zero term.","line":819,"path":"src/CDPVault.sol","reproduction":"test/scratch/StaleLegZeroesSecured.t.sol (passes on this code: it demonstrates the state). WorkBackingFixture (ParameterizedVault, $1 per collateral unit, Chainlink etched at CHAINLINK_ETH_USD). BORROWER locks 200e18 and draws 100e18; two days pass, a wipe(1e18) checkpoint warms the lag: securedCollateral ~198e18, backingPerUnit() == 1e18. vm.mockCallRevert on the aggregator's latestRoundData: collateralPriceFeed.isStale() is true, draw(1e18) reverts; BORROWER wipe(1e18) succeeds and securedCollateral becomes 0. Clear the mock and refresh the answer: isStale false, collateralRatio(BORROWER) >= 200, yet backingPerUnit() == 0 (EXPECTED 1e18) and cash(1e18, 0, BORROWER) reverts ZeroAmount. BORROWER wipe(1e18) again: backingPerUnit still 0 (lag warms from zero); one day later it reads 1e18 again.","severity":"low","snippet":"        if (principal == 0 || price == 0) return 0;","title":"A reverting ETH/USD (or share-vault) leg reads as price 0, and a wipe/lock made while it is down re-prices the position's secured term at zero; the zero persists past recovery and shuts or underpays `"},{"citation":"resolved","description":"Merged from audit_permissions and audit_economics (duplicates). Since cc4103f the guard is per epoch: every value accepted within maxAge of an epoch's start must lie within the epoch's allowance of the ANCHOR (the value held when the epoch opened); the allowance is the cap only for an epoch opened on a fresh value, STALE_DEVIATION_MULTIPLE x cap and growing on a stale one, and a wide epoch additionally holds later values to the cap around its first value (`_checkValue`, `_epoch`, `_allowanceNow`, `_epochFirst`). The @param line still states the pre-epoch rule, and a reader sizing the cap from it would conclude 20% means at most 20% between consecutive accepted values, which is false in both directions. Documentation only. Fix: '@param maxDeviationBps_ The per-epoch deviation cap in bps against the epoch's anchor when the epoch opens on a fresh value; wider on a stale one (see `_checkValue`, `_allowanceNow`), 0 to 10,000.'","line":156,"path":"src/SwarmFeed.sol","reproduction":"Feed with maxAge 1h and cap 2000 seeded V at T0. At T0+30m accept 1.2V (within the cap of the anchor V). At T0+40m submit 1.44V: EXPECTED per the @param text (20% from the LAST accepted value 1.2V) accepted; ACTUAL ExcessDeviation, because the anchor is V and 1.44V is 44% from it. Conversely, test/SwarmFeed.t.sol test_staleValueReanchorsOnlyWithinTheWidenedBound: with cap 1000, a value 20% above the last accepted value is accepted after a two-hour silence, so the cap is not 'the maximum change from the last accepted value'.","severity":"info","snippet":"    /// @param maxDeviationBps_ Maximum change from the last accepted value, from 0 to 10,000 bps.","title":"SwarmFeed constructor NatSpec describes maxDeviationBps_ as a bound on the change from the LAST accepted value; the shipped bound is per epoch, measured from the epoch's anchor and widening with stale"},{"citation":"resolved","description":"From the audit_math specialist, confirmed by reading lines 221-222 against `_checkValue`/`_epoch`/`_allowanceNow` (370-420) and DeploymentConfig.sol ATTESTATION_RELAYER. The bound no longer lifts when the value is stale: it widens by `_allowanceNow` and is measured against the epoch's anchor, not the last accepted value. And the relayer covers nothing on the shipped feeds, since ATTESTATION_RELAYER is SwarmRelay, which admits everyone (the constructor comment at lines 181-193 says so itself; SwarmRelay.sol lines 11-13 repeat the stale claim). No code effect; the sentences could mislead a reader into thinking a stale feed is unbounded or that the relayer is a trust boundary. Fix: state the per-epoch, widening bound and that the relayer is not load-bearing; the first value is bounded by nothing on chain (see the low finding at line 356).","line":221,"path":"src/SwarmFeed.sol","reproduction":"Read src/SwarmFeed.sol:221-222. A stale feed (value two hours old) submitted a value 3x its anchor: EXPECTED per 'bounds ... while the previous value is fresh' (i.e. no bound once stale) accepted; ACTUAL ExcessDeviation (allowance 4000 bps). A stranger calling SwarmRelay.relay on an unseeded shipped feed: EXPECTED per 'a nonzero relayer covers the unseeded first value' refused; ACTUAL accepted (test/scratch/FirstValueRace.t.sol).","severity":"info","snippet":"    /// The deviation guard bounds a wrong-question figure once seeded while the previous value is fresh;\n    /// a nonzero relayer covers the unseeded first value and stale re-anchors. A feed that pins its","title":"SwarmFeed.submitAttestation NatSpec still describes the pre-epoch guard: a bound that holds only 'while the previous value is fresh' and a relayer that 'covers the unseeded first value and stale re-an"},{"citation":"resolved","description":"From the audit_permissions specialist, confirmed against src/SwarmFeed.sol: ATTESTATION_TYPEHASH (lines 117-119) covers `...bytes32 panelJobId,uint16 panelSize,uint16 quorum,uint16 agreed,uint64 issuedAt,uint64 expiresAt` and DOMAIN_SEPARATOR (166-174) hashes version \"2\". ABI.md, the integrator-facing reference, documents the v1 shape (12 fields, `panelJobId` followed directly by `issuedAt`, domain version `1`), omits the panel floors (MIN_PANEL_SIZE 25, MIN_AGREED 15) the v2 fields enable, and the next paragraph (line 86) still says 'there is no immutable questionHash gate or getter' although `expectedQuestionHash(fromBlock, toBlock)` and `_requireQuestion` exist. An integrator encoding `submitAttestation` from this page produces calldata that does not decode or a digest the feed rejects. Documentation only; regenerate the paragraphs from the struct, the domain and `_requireQuestion` in SwarmFeed.sol.","line":84,"path":"docs/ABI.md","reproduction":"Build the tuple as ABI.md line 84 lists it (12 members), sign under EIP712Domain('IdentityMD Oracle','1',chainid,feed) with the attester key and call `submitAttestation` on any shipped feed: EXPECTED per the doc accepted; ACTUAL the call fails in ABI decoding (the struct has 15 members), and with the three fields added but version '1' it reverts InvalidSignature because DOMAIN_SEPARATOR hashes version '2'.","severity":"info","snippet":"`submitAttestation` takes an `OracleAttestation` tuple in this exact order: `(bytes32 requestId, uint256 chainId, bytes32 questionHash, uint8 answerType, bytes answer, uint256 figure, uint64 fromBlock, uint64 toBlock, bytes32 blockHash, bytes32 panelJobId, uint64 issuedAt, uint64 expiresAt)`, followed by a 65-byte signature. The EIP-712 domain is `IdentityMD Oracle`, version `1`, with the deployment chain ID and the receiving feed's address, computed once in its constructor. Payload chainId and answerType must equal `attestationChainId()` and `attestationAnswerType()`; the payload data chain may differ from the consumer chain (for example, mainnet data consumed on Sepolia). requestId is consumed once per feed; issuedAt cannot be in the future, exceed expiresAt, precede the last accepted update, or be older than maxAge. Delivery after expiresAt is rejected. The feed publishes figure and uses signed issuedAt for freshness, then discards any unfinished reporter round.","title":"docs/ABI.md describes the attestation as a 12-field tuple under EIP-712 domain version '1' and says there is no questionHash gate or getter; SwarmFeed signs 15 fields (panelSize, quorum, agreed added)"}],"hash":"5b558cf512f0f43527e77b8bfec686a4a631a2522fa1160237f59918235cef00","nodeId":"50d51f67-011a-4c2b-8d26-6f1b4256a6b2","outcome":"completed","summary":"All seven findings validate against the tree; `src/SwarmFeed.sol` is back to the committed state and only `.imd-findings.json` and `test/scratch/` were written.\n\n## Judgement\n\n**Kept (7 findings in `.imd-findings.json`)**\n\n| Sev | Where | Verdict |\n|---|---|---|\n| medium | `SwarmFeed._allowanceNow` L415 | **Merged from 4 specialists.** Stale base measured from signed `issuedAt`, not acceptance; a buyer holding each attestation ~55 min relays into 4,000 bps every ~65 min (3.29x in 4h15m vs committed 2.07x). All three specialist proofs fail on this code; with the `since = max(_anchorAt, _updatedAt)` patch they pass and the SwarmFeed/OracleAsker/QuestionBinding/RelayBundling/SwarmRelay suites still pass (88/88). Proof attached (audit_math's, self-contained). |\n| low | `OracleAsker.onOracleResult` L284 | A hand-relayed `askPaid` answer makes the callback's refusal set `lastAsk`, blocking Treasury `ask` for 2h at ~$5/cycle. Reproduced in `test/scratch/PaidRefusalBacksOffTreasury.t.sol`. |\n| low | `SwarmFeed._checkValue` NatSpec L356 | First value is unbounded and SwarmRelay is permissionless; DeployMainnet does not seed in the deploy tx and `verify()` never checks the anchor. Reproduced in `test/scratch/FirstValueRace.t.sol` (deployer's honest value refused for 6h). |\n| low | `CDPVault._secured` L819 | Reverting Chainlink leg → `_priceOrZero()==0` → ungated `wipe` writes a zero secured term that persists past recovery; `backingPerUnit` 0, `cash` reverts `ZeroAmount` for up to a day. Reproduced in `test/scratch/StaleLegZeroesSecured.t.sol`. Kept as a consumer effect under Q5. |\n| info ×3 | SwarmFeed L156 (merged duplicate), L221–222, `docs/ABI.md` L84 | NatSpec/doc claims the code does not have. |\n\n**Dropped:** none — every specialist finding reproduced. Duplicates merged: 4→1 (medium), 2→1 (constructor NatSpec).\n\n**Numbered questions, briefly**\n1. Acceptance is sound: domain binds chain + feed address; `usedRequests` per feed; issuedAt bounded both ways; floors enforced; question hash spliced from signed window with span/advance/recency bounds. No cross-question/chain/feed/window acceptance found.\n2. N attestations in one lifetime: the epoch allowance (cap when fresh). After H hours of silence: 4,000 + 250·(H−2) bps for H ≥ 2 (table in `_allowanceNow` is arithmetically right). Sustained rate **as committed is not the cap per hour** — see the medium. Packing (64+8+64+32+88 = 256 bits, `MAX_ALLOWANCE_BPS` < 2³²) and the unseeded case are correct; `SwarmWorkOracle._checkValue` correctly keeps only the zero check; NHI inherits the same hold weakness (one day).\n3. `ask`/`wideOpen`/arming logic correct; pool slot `keccak256(abi.encode(poolId, 6))` and inversion correct. The one gap is the back-off (low).\n4. Costing unchanged from PARAMETERS except that the medium roughly halves the attacker's exposure time to 3.48x (4–5h vs 6–7h); pool-fee cost (~$39–50k) unchanged.\n5. Units correct (`imdEth × ethUsd / 10^dec`, `rate × assetUsd / 1e18`); staleness propagates via `isStale`; a reverting leg degrades to zero rather than reverting — with the persistence side-effect in the low above.\n6. `tail()`, `_requireFreshFeeds`, `_requirePriceAgreement` check all three feeds; relay bundles strand nothing (`relayAndBite` asserts both balance deltas, `relayAndBark` uses `barkFor`); work oracle rights cannot be claimed twice (monotone `creditedTasks`) or for others (`isController`); old roots can only under-credit.\n\n**Coverage:** read in full — SwarmFeed, OracleAsker, SwarmRelay, PriceFeed, SpotFeed, NhiFeed, UsdPriceFeed, SharePriceFeed, SwarmWorkOracle, DeploymentConfig; CDPVault/ParameterizedVault read for the price-consuming paths (`_resecure`, `_backingPerUnit`, `_requireFreshFeeds`, `_requirePriceAgreement`, `barkFor`) and DeployMainnet/runbook §7 for the seeding order. Not read in full: CDPVault/ParameterizedVault/Treasury beyond those paths.","treeHash":null,"usage":{"cachedInputTokens":4542895,"inputTokens":72,"model":"claude-fable-5-1","outputTokens":38315,"runtime":"claude","turns":36,"wallClockMs":697793}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"7f1dec5ffcbde1d8","findings":[{"citation":"resolved","description":"The second-half fix (b73a05f) makes the stale base (STALE_DEVIATION_MULTIPLE x cap) require a whole STALE_GROWTH_PERIOD of staleness past the lifetime, and the NatSpec (lines 27-30, 47-51, 411-414) and docs/PARAMETERS claim that a feed anyone keeps alive moves at most the cap per hour however it is driven. But staleness is measured from `_updatedAt`, which `_accept` sets to the attestation's SIGNED `issuedAt`, and `submitAttestation` accepts an issuedAt up to `maxAge` old (line 233, `_tooOld(a.issuedAt)`), i.e. 59 minutes for the price and spot feeds. The epoch, by contrast, opens at the RELAY block (`_anchorAt = block.timestamp`, line 455) and lasts maxAge from there. A buyer who holds each attestation ~55 minutes before relaying (the window stays acceptable for 300 blocks past toBlock, `expiresAt` is issuedAt + 86400 per the frozen bodies' validForSeconds) therefore has, 65 minutes after the relay: the epoch expired (65 > 60) AND `block.timestamp - _updatedAt - maxAge` = 65 + 55 - 60 = 60 minutes = one whole period, so `_allowanceNow` returns 2x the cap. Each step of the walk is then 40% (at the launch cap of 2,000 bps) every 65 minutes: 1.2 x 1.4^k, i.e. 1.68x at 2h05, 2.35x at 3h10, 3.29x at 4h15, 4.61x at 5h20, against the documented 1.2^n per hour (2.07x at 4h, 2.49x at 5h). The honest-side effects are the same clock: an honest refresh relayed late reads stale sooner and reaches WIDE_ALLOWANCE_BPS up to an hour earlier. On the one-day NHI feed the same hold is up to 23 hours, so one relayed-late NHI value makes the next epoch open on the 40% base a day earlier than the table in `_allowanceNow` states. Reachable with the constants as committed by anyone who buys attestations off chain (documented as permitted: 'anyone can still buy an attestation off chain and relay it through SwarmRelay'); no privileged role is needed. The smallest fix is to measure silence from the later of the signed issue time and the epoch's open (the last relay the feed has recorded): in `_allowanceNow`, `uint256 since = _anchorAt > _updatedAt ? _anchorAt : _updatedAt; if (block.timestamp - since <= maxAge) return maxDeviationBps; uint256 periods = (block.timestamp - since - maxAge) / STALE_GROWTH_PERIOD;` (the scratch test passes with exactly that patch). A mid-epoch acceptance still does not help the attacker: it must sit within the cap of the epoch's anchor, so it gains nothing the epoch did not already allow.","line":415,"path":"src/SwarmFeed.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\n\n/// @dev Unbound leaf (no question prefix) with this test as its relayer, so the suite can sign as the\n/// attester. Nothing else differs from the shipped feeds: same _accept, same _allowanceNow.\ncontract Leaf is SwarmFeed {\n    constructor(address attester_, address relayer_, uint256 maxAge_, uint256 cap_)\n        SwarmFeed(attester_, relayer_, 1, 3, maxAge_, cap_)\n    {}\n}\n\n/// @notice The stale base is \"earned by a whole hour of silence past the lifetime\" (SwarmFeed NatSpec,\n/// _allowanceNow). Silence is measured from the attestation's SIGNED issuedAt, which the relayer may\n/// hold back by up to maxAge (submitAttestation accepts issuedAt up to maxAge old). A buyer who relays\n/// each step with issuedAt 55 minutes old therefore reaches the 2x stale base 65 minutes after relaying,\n/// not two hours, and compounds at 1.4 per 65 minutes instead of the documented 1.2 per hour.\ncontract StaleBaseFromIssuedAtTest is Test {\n    uint256 private constant KEY = 0xA11CE;\n    Leaf private feed;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.warp(10 days);\n        vm.roll(1_000_000);\n        feed = new Leaf(vm.addr(KEY), address(this), 1 hours, 2_000);\n    }\n\n    function test_backdatedIssuedAtEarnsTheStaleBaseWithinOneLifetimeOfTheLastRelay() public {\n        uint256 t0 = block.timestamp;\n        // Honest value, signed and relayed now.\n        _relay(\"seed\", 1 ether, uint64(t0));\n        uint256 cap = feed.maxDeviationBps();\n\n        // Step 1 at t0 + 1h: the epoch has run its lifetime, the allowance is the cap. The attacker's\n        // attestation was issued 55 minutes ago (held back; still within maxAge, window still recent).\n        vm.warp(t0 + 1 hours);\n        assertEq(_allowance(), cap, \"first step: the cap\");\n        _relay(\"step-1\", 1.2 ether, uint64(block.timestamp - 55 minutes));\n\n        // 65 minutes after that relay the feed has been kept alive for one lifetime and five minutes.\n        // Documented: \"a feed anyone keeps alive moves at most the cap per hour however it is driven\",\n        // so the next epoch must open on the cap. Actual: silence is counted from the backdated issuedAt,\n        // the value has been \"stale\" for a whole hour, and the stale base (2x the cap) is granted.\n        vm.warp(block.timestamp + 65 minutes);\n        // The walk takes it: 1.2 x 1.4 = 1.68 in two hours and five minutes (the cap per hour allows 1.44).\n        bool accepted = _tryRelay(\"step-2\", 1.68 ether, uint64(block.timestamp - 55 minutes));\n        assertFalse(accepted, \"1.68 after two steps 65 minutes apart exceeds the cap per hour; must be refused\");\n        assertEq(_allowance(), cap, \"65 minutes after a relay the allowance must still be the cap\");\n    }\n\n    function _tryRelay(bytes32 id, uint256 figure, uint64 issuedAt) private returns (bool ok) {\n        SwarmFeed.OracleAttestation memory a = _build(id, figure, issuedAt);\n        (ok,) = address(feed).call(abi.encodeCall(feed.submitAttestation, (a, _sign(a))));\n    }\n\n    function _allowance() private view returns (uint256 allowance) {\n        (,, allowance) = feed.epoch();\n    }\n\n    function _relay(bytes32 id, uint256 figure, uint64 issuedAt) private {\n        SwarmFeed.OracleAttestation memory a = _build(id, figure, issuedAt);\n        feed.submitAttestation(a, _sign(a));\n    }\n\n    function _build(bytes32 id, uint256 figure, uint64 issuedAt)\n        private\n        view\n        returns (SwarmFeed.OracleAttestation memory a)\n    {\n        a.requestId = id;\n        a.chainId = 1;\n        a.questionHash = keccak256(\"q\");\n        a.answerType = 3;\n        a.answer = bytes(\"x\");\n        a.figure = figure;\n        a.fromBlock = uint64(block.number - 600);\n        a.toBlock = uint64(block.number - 1);\n        a.blockHash = keccak256(\"b\");\n        a.panelJobId = keccak256(\"p\");\n        a.panelSize = 60;\n        a.quorum = 20;\n        a.agreed = 40;\n        a.issuedAt = issuedAt;\n        a.expiresAt = issuedAt + 1 days;\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                    feed.ATTESTATION_TYPEHASH(),\n                    a.requestId,\n                    a.chainId,\n                    a.questionHash,\n                    a.answerType,\n                    keccak256(a.answer),\n                    a.figure,\n                    a.fromBlock\n                ),\n                abi.encode(\n                    a.toBlock, a.blockHash, a.panelJobId, a.panelSize, a.quorum, a.agreed, a.issuedAt, a.expiresAt\n                )\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) =\n            vm.sign(KEY, keccak256(abi.encodePacked(\"\\x19\\x01\", feed.DOMAIN_SEPARATOR(), body)));\n        return abi.encodePacked(r, s, v);\n    }\n}","reproduction":"Leaf feed (unbound question, test is the relayer), maxAge 1 hours, cap 2_000, chain id 1. T0: relay figure 1e18 with issuedAt = T0. T0+1h: epoch().allowanceBps == 2000; relay 1.2e18 with issuedAt = T0+1h-55min (accepted: issuedAt >= _updatedAt, not too old). T0+2h05m: expected (NatSpec 'at most the cap per hour however it is driven'): epoch().allowanceBps == 2000 and figure 1.68e18 refused (ExcessDeviation; 1.44e18 is the most two cap-steps allow). Actual: epoch().allowanceBps == 4000 and 1.68e18 is accepted with issuedAt = now-55min. Repeating every 65 minutes gives 1.2 x 1.4^k. test/scratch/StaleBaseFromIssuedAt.t.sol fails on the committed code (assertion '1.68 after two steps 65 minutes apart exceeds the cap per hour; must be refused') and passes once `_allowanceNow` measures silence from max(_updatedAt, _anchorAt).","severity":"medium","snippet":"        uint256 periods = (block.timestamp - _updatedAt - maxAge) / STALE_GROWTH_PERIOD;","title":"SwarmFeed._allowanceNow counts the stale base's hour of silence from the relayer-chosen issuedAt, so a walk relayed with 55-minute-old attestations compounds at 2x the cap per 65 minutes instead of th"},{"citation":"resolved","description":"`_checkValue` applies no deviation bound while `_hasValue` is false, and the NatSpec here (and at lines 221-222: 'a nonzero relayer covers the unseeded first value and stale re-anchors') says that first value is the deployer's. On mainnet the pinned relayer is SwarmRelay, which admits every caller (the constructor comment at lines 181-193 says so), the question bodies are public (deploy/mainnet/bodies, `{{FEED}}` filled with the feed address), and the feed addresses are planned CREATE2 addresses. So the first accepted value of PriceFeed, SpotFeed and NhiFeed is set by whoever relays first, and the vault (ParameterizedVault is permissionless from its constructor; runbook 7.5 'only then open deposits' is an announcement, not an on-chain gate) prices every position off that anchor. A first value at 2x the market is a signed, honest answer to the pinned question if the pool was held at 2x for ~65 of the window's 120 minutes (7 of 13 samples), i.e. the Q4 ramp-and-hold without any prior walk and without waiting for a silence. The deployer's honest attestation (market, 0.5x the anchor) is then refused with ExcessDeviation: the first epoch is cap-bounded for an hour, and afterwards `_allowanceNow` needs 5000 bps, which is six hours after the attacker's issuedAt (five, with the hold of finding 1). The runbook's 7.1 ('buy one attestation per feed and relay it ... verify on chain that each feed's value is the attested figure') has no step for this and nothing in `verify()` checks the anchor against the pool. Reachable with the constants as committed; it is a race the attacker must win in the minutes between deployment and the deployer's relay, with a pumped pool held through that window, so low rather than medium. Smallest fixes, either: have DeployMainnet buy the three attestations for the planned addresses before the broadcast and relay them in the same transaction as the deployment (the attester signs for the consumer address in the body, which the plan already fixes), so no block exists in which the feeds are unseeded; or make the first epoch's anchor checkable, e.g. `verify()` requires `|priceFeed.latestValue() - asker.poolPrice()| <= cap` before the operator opens deposits. The NatSpec at 356-357 and 221-222 should say that the first value is bounded by nothing on chain and is whoever's relays first.","line":356,"path":"src/SwarmFeed.sol","reproduction":"State: PriceFeed deployed, no value. Anyone (not the deployer) calls SwarmRelay.relay(priceFeed, a, sig) with a valid attester-signed attestation whose figure is 2V while the market is V (the window's median after a 65-minute hold). Expected per the NatSpec: the first value is the deployer's, bought and checked. Actual: accepted (no bound while _hasValue is false; relayer is SwarmRelay, which forwards for anyone); `epoch()` then reports anchor 2V, allowance 2000; the deployer's own attestation with figure V reverts ExcessDeviation (|V - 2V| = V > 0.2 x 2V) in the epoch's hour and for every later purchase until the allowance reaches 5000 bps (periods >= 5, i.e. six hours after the attacker's issuedAt). With SpotFeed seeded from the same window and NhiFeed seeded honestly, every price action on the vault is live at 2V: lock sIMD worth $850k at market, draw 1,000,000e18 imdUSD (LINE), collateral ratio reads 170.","severity":"low","snippet":"    /// stale (`_allowanceNow`). The first value ever has no\n    /// bound: it is the one we buy and check at deployment, and it anchors the first epoch.","title":"SwarmFeed: the unbounded first value is 'the one we buy and check at deployment' only by convention; with SwarmRelay as the pinned relayer anyone can anchor a freshly deployed feed first, and the depl"}],"hash":"6572c71642e6535f5e669ef3a8fe4e0a9d93491e80dac65cfcc4540ebbce170d","nodeId":"cb20d9d9-477a-47d5-bcb1-12ffb844aafe","outcome":"completed","summary":"`git status` shows no tracked file changed; the only additions are `.imd-findings.json` and the scratch test. Review complete.\n\n## Result\n\nTwo findings, written to `.imd-findings.json`: one **medium** (with a Foundry proof that fails on the committed code and passes under the one-line fix), one **low**. Everything else in scope held up.\n\n| # | Sev | Where | Finding |\n|---|---|---|---|\n| 1 | medium | `src/SwarmFeed.sol:415` `_allowanceNow` | The hour of silence that earns the stale base is counted from the attestation's **signed `issuedAt`**, which the relayer may hold back by up to `maxAge`. The epoch, however, opens at the **relay block**. A buyer relaying each step with a ~55-minute-old attestation (window still within 300 blocks, `validForSeconds` 86400) gets the 2x base 65 minutes after each relay: the walk compounds at **1.4 per 65 min**, not the documented 1.2 per hour (3.29x at 4h15 vs 2.49x at 4h). Regression gap in the b73a05f fix; its test only relays with `issuedAt == block.timestamp`. Fix: measure silence from `max(_updatedAt, _anchorAt)`. Proof: `test/scratch/StaleBaseFromIssuedAt.t.sol`. |\n| 2 | low | `src/SwarmFeed.sol:356` | The unbounded first value is \"the one we buy and check at deployment\" only by convention: SwarmRelay admits everyone, bodies and CREATE2 addresses are public, the vault is live from its constructor. Whoever relays first anchors the feed; a pumped-window 2x anchor refuses the deployer's honest value for ~6h. A race, hence low. |\n\n## Answers to the numbered questions\n\n**1. Attestation acceptance.** Sound. Domain binds chain id and feed address (cross-feed/cross-chain replay impossible); `requestId` replay; `issuedAt ≤ now`, `≤ expiresAt`, not older than `maxAge`, never older than the current value; panel floors 25/15 with `agreed ≤ panelSize`; the question is rebuilt from the pinned prefix plus the signed window, span-bounded, advancing, and recent on the data chain. No different question/chain/feed/window can land. Low-s and v ∈ {27,28} enforced. The one thing issuedAt latitude buys an attacker is finding 1.\n\n**2. Epoch bound.** Within one lifetime: at most the epoch allowance from the anchor (a wide epoch is then held to the cap around its first value, so the first relayer after a silence gets the allowance, nobody after). After H hours of silence: 20% for H<2, then 40% + 2.5%·(H−2), capped at 10,000% (reached at ~3,986 h), so no gap can *never* be followed, only delayed. Sustained rate as committed: cap/hour **only if attestations are relayed promptly**; with held attestations it is 2x cap per 65 min (finding 1). Packing is exact (bound ≤ 1e6 fits uint32; `_epochFirst` falls back to anchor-only above 2^88, unreachable for 1e15–1e18 figures). Unseeded: `_hasValue` false skips the bound, `epoch()` reports max allowance, `wideOpen` true — as documented. `SwarmWorkOracle._checkValue` keeps only the zero check; the base's epoch bookkeeping still runs harmlessly (roots exceed uint88, so `_epochFirst` is 0). NHI under the hourly schedule: a >20% index step is refused until 25h after the last `issuedAt`, so one to five hours of halt per step; a held NHI attestation cannot make the feed older than it already is (`issuedAt ≥ _updatedAt`), so no staleness DoS.\n\n**3. OracleAsker.** Correct as reviewed: `ask` pays only for keep-alive near-stale, `wideOpen` (stale ∧ no live epoch ∧ allowance ≥ 6000), or an armed fall still present 5–100 blocks later; rises never. `askPaid`/`askPaidMany` charge only for requests made. `_request` keeps `feedOf` so late deliveries land; `onOracleResult` is Intake-only, never reverts past the clearing, backs off only the live request. The pool slot is correct for v4 (`_pools` at slot 6, sqrtPriceX96 in the low 160 bits, inversion exact). Treasury exposure is bounded by the 15 IMD/day top-up; making it pay without cause needs two ~$1.2k pool pushes five blocks apart for a $5 spend (documented, accepted). Nothing blocks updates for everyone (hand relay always works), and a lo","treeHash":null,"usage":{"cachedInputTokens":6423409,"inputTokens":82,"model":"claude-fable-5-1","outputTokens":74447,"runtime":"claude","turns":44,"wallClockMs":1067055}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"b4f6137e7c93a2b3","findings":[{"citation":"resolved","description":"Q2. The fix in b73a05f for the second-half review's medium (the stale base earned one second past the lifetime) measures 'whole periods stale' from `_updatedAt`. `_accept` sets `_updatedAt` to the attestation's SIGNED `issuedAt` (line 463), while the epoch is dated from the block it was relayed in (`_anchorAt = block.timestamp`, line 455). `submitAttestation` accepts an issuedAt up to one lifetime old (line 233, `_tooOld(a.issuedAt)`), and the window check only needs toBlock within maxAge/12 = 300 blocks of head (line 309). So an attestation bought off chain with a window ending at head and relayed by hand ~55 minutes later (the hold is the hour minus the panel's answer latency) is accepted with issuedAt ~55 minutes in the past, and one hour and five minutes after that relay the feed has been 'stale' for a whole STALE_GROWTH_PERIOD by its own clock: `_allowanceNow` returns STALE_DEVIATION_MULTIPLE x cap = 4,000 bps although the feed was relayed 65 minutes ago. Every epoch after the first therefore opens on the 40% base, and the walk compounds at 1.4 per hour-and-five-minutes instead of 1.2 per hour: 1.2 x 1.4^3 = 3.29x four relays after a fresh seed (4h15m) against the documented 1.2^4 = 2.07x; 3.48x is passed on the fifth relay (~5h20m) rather than the seventh (6h). The last step is relayed promptly (issuedAt = now) so the end value is fresh for the vault. The spot feed is walked the same way in parallel so the SKEW_BPS guard holds. The NatSpec claims the property the code does not have: lines 27-30 ('The stale base is earned by silence, never by timing ... a feed anyone keeps alive moves at most the cap per hour however it is driven'), lines 47-49 ('with the stale base earned only by a whole hour of silence past the lifetime, a run of steps compounds at no more than the cap per hour after the first'), and lines 412-414 in _allowanceNow. The regression test test_relayingAnHourApartNeverEarnsTheStaleBase only relays with issuedAt == block.timestamp. Also affected: `OracleAsker.wideOpen` and the H-hours-of-silence allowance read one hour further down the table than the relay silence justifies (a value relayed 9 hours ago with a held issuedAt already reads WIDE_ALLOWANCE_BPS). Reachable with the constants as committed (cap 2,000, maxAge 1 hour, span 300-1,200, recency 300 blocks); the attacker needs to buy attestations off chain and relay by hand (the live attestation in oracle/attestation-e2c85027.json is signed with expiresAt - issuedAt = 86,400 s, so a 55-minute hold is well inside the signature's validity), which docs/MAINNET-RUNBOOK.md and SwarmFeed's own NatSpec describe as the normal fallback. Cost: the pool must be held at each rung during the sampled window, the same ramp-and-hold round trip (~$39-50k at 841 ETH / 207,881 IMD, 1% fee) the earlier reviews costed, now for 4-5 hours instead of 6+; at LINE $1M and mat 170 the 3.29x walk lets $1M be drawn against collateral worth ~$516k at market. Smallest fix: in `_allowanceNow` measure the silence from the later of the signed time and the epoch's open, `uint256 since = _anchorAt > _updatedAt ? _anchorAt : _updatedAt; if (block.timestamp - since <= maxAge) return maxDeviationBps; uint256 periods = (block.timestamp - since - maxAge) / STALE_GROWTH_PERIOD;` (both are already in the slot `_accept` writes; test/SwarmFeed.t.sol still passes with this change). A stricter alternative is to bound `block.timestamp - a.issuedAt` in submitAttestation to the panel's real latency, but that changes acceptance.","line":415,"path":"src/SwarmFeed.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\n\n/// @dev The PriceFeed's launch policy (1-hour lifetime, 2,000 bps cap) with an attester whose key this\n/// test holds and no pinned question, so only the epoch bound is exercised.\ncontract HeldFeed is SwarmFeed {\n    constructor(address attester_, address relayer_) SwarmFeed(attester_, relayer_, 1, 3, 1 hours, 2_000) {}\n}\n\n/// @notice FINDING: the stale base is earned by the age of the signed `issuedAt`, not by silence of the\n/// feed. `_allowanceNow` measures \"whole periods stale\" from `_updatedAt`, which `_accept` sets to the\n/// attestation's signed issue time, while the epoch is measured from the block the value was relayed in.\n/// `submitAttestation` accepts an issuedAt up to one lifetime old, so a buyer who holds every answer for\n/// most of an hour before relaying it opens every epoch on a value the feed believes has been stale for\n/// a whole hour, and takes STALE_DEVIATION_MULTIPLE x the cap (40%) at every step, one step an hour.\n///\n/// The NatSpec (SwarmFeed.sol:27-30, 47-49, 410-414) says the stale base is \"earned by silence, never by\n/// timing\" and that \"a feed anyone keeps alive moves at most the cap per hour however it is driven\".\n/// This test keeps the feed alive with one relay an hour and moves it 1.2 x 1.4^3 = 3.29x in four hours\n/// where the cap per hour allows 1.2^4 = 2.07x. It fails on the committed code and passes once the\n/// allowance's staleness is measured from the later of the signed time and the epoch's open (the relay).\ncontract StaleBaseByHeldAnswerTest is Test {\n    uint256 private constant ATTESTER_KEY = 0xA11CE;\n    uint256 private constant V = 3_550_000 gwei; // ~0.00355 ETH per IMD, the live figure\n    uint256 private constant HOLD = 1 hours - 5 minutes; // the panel answers minutes after the window closes\n    HeldFeed private feed;\n    uint256 private nonce;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.warp(10 days);\n        vm.roll(1_000_000);\n        feed = new HeldFeed(vm.addr(ATTESTER_KEY), address(this));\n        // Honest seed, issued now.\n        _submit(V, uint64(block.timestamp));\n    }\n\n    /// @dev One relay an hour (plus the five minutes the hold leaves), every answer held HOLD before it\n    /// is relayed. Expected under the documented bound: each step is at most the cap (20%). Actual: the\n    /// second step onwards opens on the 40% stale base.\n    function test_aFeedKeptAliveHourlyMovesAtMostTheCapPerHour() public {\n        uint256 cap = feed.maxDeviationBps();\n        uint256 value = V;\n        // Step 1, an hour after the seed: the epoch has run its lifetime, the allowance is the cap.\n        _advance(1 hours);\n        value = value * (10_000 + cap) / 10_000;\n        _submit(value, uint64(block.timestamp - HOLD));\n        // Steps 2-4: each relayed one hour and five minutes after the previous relay, so the feed has\n        // never been silent for a whole hour past its lifetime, yet its allowance reads 2x the cap.\n        for (uint256 i; i < 3; ++i) {\n            _advance(1 hours + 5 minutes);\n            (,, uint256 allowance) = feed.epoch();\n            assertLe(allowance, cap, \"an hourly-relayed feed must not open its epoch on the stale base\");\n            // What the code actually lets through: a 40% step.\n            uint256 attempt = value * (10_000 + 2 * cap) / 10_000;\n            bool accepted = _try(attempt, uint64(block.timestamp - HOLD));\n            assertFalse(accepted, \"a 40% step was accepted an hour after the last relay\");\n            // Keep the feed alive either way: the honest cap step is what an hourly relayer sends.\n            value = accepted ? attempt : value * (10_000 + cap) / 10_000;\n            if (!accepted) _submit(value, uint64(block.timestamp - HOLD));\n        }\n    }\n\n    /// @dev The same walk expressed as a bound on the end value: four hourly relays may multiply the\n    /// feed by at most 1.2^4 = 2.0736 with a 2,000 bps cap.\n    function test_fourHourlyRelaysMoveTheFeedAtMostTheCapToTheFourth() public {\n        uint256 value = V;\n        _advance(1 hours);\n        value = value * 12 / 10;\n        _submit(value, uint64(block.timestamp - HOLD));\n        for (uint256 i; i < 3; ++i) {\n            _advance(1 hours + 5 minutes);\n            uint256 attempt = value * 14 / 10;\n            if (_try(attempt, uint64(block.timestamp - HOLD))) value = attempt;\n        }\n        (uint256 got,) = feed.latestValue();\n        assertLe(got, V * 20_736 / 10_000 + 1, \"four hourly relays walked the feed past 1.2^4\");\n    }\n\n    function _advance(uint256 secs) private {\n        vm.warp(block.timestamp + secs);\n        vm.roll(block.number + secs / 12);\n    }\n\n    function _submit(uint256 figure, uint64 issuedAt) private {\n        SwarmFeed.OracleAttestation memory a = _attestation(figure, issuedAt);\n        feed.submitAttestation(a, _sign(a));\n    }\n\n    function _try(uint256 figure, uint64 issuedAt) private returns (bool ok) {\n        SwarmFeed.OracleAttestation memory a = _attestation(figure, issuedAt);\n        bytes memory sig = _sign(a);\n        try feed.submitAttestation(a, sig) {\n            ok = true;\n        } catch (bytes memory reason) {\n            assertEq(bytes4(reason), SwarmFeed.ExcessDeviation.selector, \"refused for another reason\");\n        }\n    }\n\n    function _attestation(uint256 figure, uint64 issuedAt) private returns (SwarmFeed.OracleAttestation memory a) {\n        a.requestId = keccak256(abi.encode(\"request\", ++nonce));\n        a.chainId = 1;\n        a.questionHash = keccak256(\"question\");\n        a.answerType = 3;\n        a.answer = abi.encode(figure);\n        a.figure = figure;\n        a.fromBlock = uint64(block.number - 300);\n        a.toBlock = uint64(block.number);\n        a.blockHash = keccak256(\"block\");\n        a.panelJobId = keccak256(\"panel\");\n        a.panelSize = 60;\n        a.quorum = 20;\n        a.agreed = 40;\n        a.issuedAt = issuedAt;\n        a.expiresAt = uint64(block.timestamp + 1 days);\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                    feed.ATTESTATION_TYPEHASH(),\n                    a.requestId,\n                    a.chainId,\n                    a.questionHash,\n                    a.answerType,\n                    keccak256(a.answer),\n                    a.figure,\n                    a.fromBlock\n                ),\n                abi.encode(\n                    a.toBlock, a.blockHash, a.panelJobId, a.panelSize, a.quorum, a.agreed, a.issuedAt, a.expiresAt\n                )\n            )\n        );\n        (uint8 v, bytes32 r, bytes32 s) =\n            vm.sign(ATTESTER_KEY, keccak256(abi.encodePacked(\"\\x19\\x01\", feed.DOMAIN_SEPARATOR(), body)));\n        return abi.encodePacked(r, s, v);\n    }\n}","reproduction":"test/scratch/StaleBaseByHeldAnswer.t.sol (FAILS on this code, passes with the fix above). HeldFeed(maxAge 1h, cap 2000) seeded V = 3.55e15 at T0 with issuedAt T0. T0+1h: relay 1.2V with issuedAt = now - 55 min (accepted, epoch opens, allowance 2000). T0+2h05m: expected feed.epoch() allowanceBps == 2000 (the feed was relayed 65 minutes ago); actual 4000. 1.68V (a 40% step, issuedAt now - 55 min) expected ExcessDeviation; actual accepted. Repeat twice: T0+3h10m 2.352V accepted, T0+4h15m 3.293V accepted (latestValue 11,689,440,000,000,000 against the cap-per-hour ceiling of 7,361,280,000,000,000).","severity":"medium","snippet":"        uint256 periods = (block.timestamp - _updatedAt - maxAge) / STALE_GROWTH_PERIOD;","title":"SwarmFeed._allowanceNow: the stale base is earned by the age of the signed issuedAt, not by silence, so a buyer who holds each answer ~55 minutes before relaying walks the feed 40% per hour (3.29x in "},{"citation":"resolved","description":"Comment audit (asked for in the brief). Two statements in the submitAttestation NatSpec (lines 221-222) and the constructor parameter doc (line 156, '@param maxDeviationBps_ Maximum change from the last accepted value') describe the guard before ce39fc6/cc4103f: the bound no longer lifts when the value is stale (it widens by _allowanceNow and is measured against the epoch's anchor, not the last accepted value), and the relayer covers nothing on the shipped feeds, since ATTESTATION_RELAYER is SwarmRelay, which admits everyone (the constructor comment at lines 181-193 says so itself). The rest of the docstring (lines 22-51) is current apart from the claim finding 1 contradicts. No code effect; the stale sentences could mislead a reader into thinking a stale feed is unbounded or that the relayer is a trust boundary.","line":221,"path":"src/SwarmFeed.sol","reproduction":"Read src/SwarmFeed.sol:156 and :221-222 against _checkValue/_epoch/_allowanceNow (lines 370-420): a value is bounded against the epoch anchor with an allowance that widens with staleness and never lifts; relayer == SwarmRelay (DeploymentConfig.sol:78), callable by anyone. Expected: NatSpec states the per-epoch, widening bound and that the relayer is not load-bearing; actual: it states the last-value bound that applies only while fresh and a relayer that covers stale re-anchors.","severity":"info","snippet":"    /// The deviation guard bounds a wrong-question figure once seeded while the previous value is fresh;","title":"SwarmFeed.submitAttestation NatSpec still describes the pre-epoch guard: a bound that holds only 'while the previous value is fresh' and a relayer that 'covers ... stale re-anchors'"}],"hash":"6751d2e885c8661fb9709557b649a15c53d6eff24f7e3196df31b88db64a79c6","nodeId":"ee33f91a-19a2-41be-bdc8-9903be5941d3","outcome":"completed","summary":"All 116 oracle-path tests pass with the fix; `src/SwarmFeed.sol` is restored and the tree has no tracked changes. `.imd-findings.json` holds two findings (one medium with a failing Foundry proof, one info) and is the deliverable.\n\n## Review of the price path at `b73a05f`\n\n**Read in full:** `SwarmFeed`, `PriceFeed`, `NhiFeed`, `SpotFeed`, `SwarmRelay`, `OracleAsker`, `UsdPriceFeed`, `SharePriceFeed`, `SwarmWorkOracle`, `DeploymentConfig`, the three interfaces, `ParameterizedVault` (price/staleness overrides, constructor), and the relevant parts of `CDPVault` (price gates, `bite`, `cash`, `_clearIfRecovered`, `tail`, `_mat/_lull`) and `Treasury` (`fundOracle`, reserve valuation). Not read: the rest of `CDPVault`/`Treasury`/`Parameters`, the question bodies under `whitepaper/` (not in the tree), and the Intake/StakedIMD/PoolManager code, which are upstream.\n\n### Findings\n\n**1. Medium — `SwarmFeed._allowanceNow` (src/SwarmFeed.sol:415): the stale base is earned by the signed `issuedAt`'s age, not by relay silence.** `_updatedAt` is the attestation's `issuedAt`, accepted up to one lifetime old; the epoch is dated from the relay block. A buyer who holds each off-chain answer ~55 minutes (the 24h signed TTL and the 300-block recency bound both allow it) opens every epoch after the first on the 40% base: 1.2·1.4³ = **3.29x in four hourly relays** against the documented 1.2⁴ = 2.07x; 3.48x on the fifth relay rather than the seventh. This is a gap left by the b73a05f fix for the second-half medium, and NatSpec lines 27-30, 47-49 and 412-414 claim the property the code lacks. Proof `test/scratch/StaleBaseByHeldAnswer.t.sol` fails on this code and passes with the smallest fix: measure the silence from `max(_updatedAt, _anchorAt)`. Same walk, same ~$39-52k pool round-trip cost, but 4-5 hours of hold instead of 6+.\n\n**2. Info — stale NatSpec** at src/SwarmFeed.sol:156 and :221-222 (last-value bound \"while fresh\"; a relayer that \"covers stale re-anchors\").\n\n### Answers where nothing is wrong\n- **Q1** Domain binds chain+feed address; `expectedQuestionHash` splices the signed window into the pinned prefix; replay by `requestId`; panel floors, `issuedAt ≤ now`, monotone `toBlock`, recency ≤ `maxAge/12`. No cross-question/chain/feed/window acceptance found.\n- **Q2** Within one lifetime N attestations move at most the epoch allowance (cap on a fresh anchor); after H hours of silence a single step gets `_allowanceNow(H)` (40% at 2h, 60% at 10h, 100% at 26h; +1h with the held-answer trick above). Packing is exact (64+8+64+32+88 bits, `uint32(bound) ≤ 1e6`, `uint88` first value ≫ any shipped figure); unseeded case reads (0, now, 1e6) with no underflow; `SwarmWorkOracle`'s override keeps the zero check and the epoch bookkeeping is harmless; NHI widens hourly from 25h. No genuine fall is unfollowable (100% at 26h); a wide epoch cannot be held open past its lifetime.\n- **Q3** `wideOpen` now needs no live epoch, so a refresh costs one purchase per ≥10h silence; drift needs two pool pushes ≥5 blocks apart (~$1-2k of fees per 0.5 IMD); budget caps at 15 IMD/day via top-up-not-pile-up; `onOracleResult` is Intake-only, never reverts past the clearing, backs off only the live request, and a timed-out request's late delivery now lands. The `extsload` slot `keccak256(poolId, 6)` and the sqrtP inversion are correct for a native-ETH/IMD v4 pool.\n- **Q4** Ramp-and-hold: pushing by M costs ≈ 2% × 841(√M−1) ETH. Over-borrowing at LINE $1M, mat 170: 3.48x → $39k fees, gain ≈ $511k, 6h hold under the committed bound (4-5h with finding 1); what stops it is only the attacker's exposure to holders selling into the pump and the SKEW guard forcing both feeds to be walked. Liquidation direction: one 20% step marks every CR-170 position; the 6h grace at NHI ≥ 0.85 is the defence, the attacker's cost is holding the pool down (~$2.7k fees each way plus arbitrage exposure) for the grace, reward ≤ 16% of liquidated debt.\n- **Q5** Units compose to USD per 1e18 raw units correctl","treeHash":null,"usage":{"cachedInputTokens":4053814,"inputTokens":60,"model":"claude-fable-5-1","outputTokens":56249,"runtime":"claude","turns":34,"wallClockMs":954651}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"54f31a82977e83fd","findings":[],"hash":"915ae3c1575f1cd4f282e594d3d220954f727623929a6459ec1316e9f7c251e2","nodeId":"ee33f91a-19a2-41be-bdc8-9903be5941d3","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":6,"wallClockMs":1332940}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"4dd67dae195771b6","findings":[{"citation":"resolved","description":"Q2 (fastest sustained rate; the NHI feed under the same schedule). `_accept` stores the attestation's SIGNED issue time as `_updatedAt` (line 463, `_updatedAt = updatedAt` = `a.issuedAt`), and `_allowanceNow` (lines 410-420) counts the whole hours of silence that earn the stale base from that timestamp. But `submitAttestation` accepts an attestation whose issuedAt is up to `maxAge` old (line 233, `_tooOld(a.issuedAt)` is false at exactly one lifetime), `_requireQuestion` accepts a window that closed up to maxAge/12 = 300 blocks ago (line 309), and the shipped bodies set `validForSeconds: 86400` (deploy/mainnet/bodies/*.template.json), so a signed answer stays relayable for a day. A relayer therefore controls when an accepted value starts being 'silent': relay an attestation signed 59-60 minutes ago and the feed's value is stale a minute later, and exactly one lifetime after that relay (when the epoch it opened expires) `periods` is already 1 and the next value may sit 2x the cap (4,000 bps) from the anchor. The committed NatSpec (lines 27-30: 'now a feed anyone keeps alive moves at most the cap per hour however it is driven'; lines 47-51: 'a run of steps compounds at no more than the cap per hour after the first'; lines 412-414: 'a value relayed an hour and a second after the last cannot open an epoch on the stale base') and docs/PARAMETERS-2026-10-05.md ('The walk ... compounds at the cap per hour after the first step ... `test_relayingAnHourApartNeverEarnsTheStaleBase` pins the rate') describe a property the code does not have: the committed test relays with issuedAt == block.timestamp, the one timing that does not exploit it. Attack sequence (external caller, mainnet constants: maxAge 1h, cap 2000, LINE $1M, mat 170): the attacker ramps and holds IMD's v4 pool at rung k during a window W_k (primary: 13 samples over the window, median = the rung once the pool holds it for the last 300 blocks; spot: last block), buys the attestation at W_k (x402 from the plane, or askPaid whose callback is refused because the figure is outside the live epoch and stays public), and relays it by hand through SwarmRelay.relay at R_k = W_k + 300 blocks, i.e. with issuedAt about 1h - delta old (delta = the panel's signing latency after the window closes, minutes). At R_k + 1h + delta the epoch has expired AND `block.timestamp - _updatedAt - maxAge >= 1h`, so `epoch()` reports 4,000 bps and 1.4x the previous value is accepted. Fresh feed at V: 1.2V at +1h (fresh cap), 1.68V at +2h+d, 2.35V at +3h+2d, 3.29V at +4h+3d, 4.61V at +5h+4d, against the documented 1.2^n (2.49V at +5h). 3.48x, the level the parameters doc costs at ~$39k of pool fees against ~$500k of over-borrowing at the $1M line, is reached in four to five hours of holding the pool instead of the documented six to seven; the honest refresh the Treasury pays for after a wide-open silence (OracleAsker.wideOpen) only buys 2h - delta of cap-bounded protection instead of two full hours, because its own callback delivery also carries an issuedAt a few minutes before the relay. For the one-day NHI feed the same rule lets a value held 23h59m be relayed with the next daily step allowed 4,000 + 250 x 22 = 9,500 bps (95%) from the anchor instead of the cap: whoever can move the live index (or a buyer of a genuinely moved reading) gets mat 200 and a zero grace in one step rather than over days. Reachable with the constants as committed; no privileged role; the only cost over the documented walk is buying each answer about an hour before relaying it. Expected: one hour after a value is ACCEPTED the allowance is still the cap (2,000). Actual: `epoch()` reports 4,000 and `submitAttestation` accepts a figure 1.4x the anchor. Smallest fix: measure the silence from the acceptance, not the signature. Since `_allowanceNow` only matters once the stored epoch has expired and every acceptance inside an epoch happened at or after `_anchorAt`, `uint256 since = _updatedAt > _anchorAt ? _updatedAt : _anchorAt; if (","line":415,"path":"src/SwarmFeed.sol","reproduction":"test/scratch/HeldAttestationEarnsStaleBase.t.sol (FAILS on this code, PASSES with the fix above). (1) WalkFeed (SwarmFeed, maxAge 1h, cap 2000, relayer = test, attester key held): submit V with issuedAt = t0. At t0+1h submit 1.2V with issuedAt = t0 (held one lifetime; accepted, fresh cap). At t0+2h, one hour after that acceptance: expected epoch().allowanceBps == 2000 and 1.68V refused (ExcessDeviation), 1.44V accepted; actual allowanceBps == 4000 and 1.68V accepted. (2) BoundWalkFeed (pinned question, span 300..1200, chain id 1, window-age check live): seed V at head; then four times: warp +1h+1m, roll +305, relay an attestation whose toBlock is head-300 (the oldest window accepted) with issuedAt = now - 60 min; expected allowance 2000 and 1.2x accepted / 1.4x refused each step; actual allowance 4000 and 1.4x accepted every step (1.4^4 = 3.84x in 4h04m).","severity":"medium","snippet":"        uint256 periods = (block.timestamp - _updatedAt - maxAge) / STALE_GROWTH_PERIOD;","title":"SwarmFeed._allowanceNow measures the 'hour of silence' from the signed issuedAt, not from acceptance, so a relayer who holds each attestation for its lifetime re-earns the 2x stale base one lifetime a"},{"citation":"resolved","description":"Q5 (what a reverting or non-standard leg does to every consumer). UsdPriceFeed._ethUsd maps an aggregator that reverts or answers malformed to (0, 0, 0), so UsdPriceFeed.latestValue returns (0, 0) and SharePriceFeed.latestValue returns (0, 0) too (SharePriceFeed also returns (0, 0) when sIMD's convertToAssets reverts). ParameterizedVault._priceOrZero then returns 0. Price actions are refused (StaleFeed), which is the documented safe direction, but `lock`, `lockIMD` and `wipe` are deliberately ungated and each calls `_resecure(position, _priceOrZero())` (lines 378, 403, 1151), and `_secured` (this line) returns 0 for a zero price: the position's whole term leaves `securedCollateral`, and `_clampLag` (line 849-850) treats that drop as a real decrease and clamps `laggedSecured` down with it. Nothing re-prices the term when the leg recovers; only that borrower's next lock/free/draw/wipe does, and the restored amount then counts only as it warms up over BACKING_WARMUP (a day). `_backingPerUnit` (line 677) therefore reads 0 for a single-borrower vault (or is depressed by that borrower's share of secured collateral in general) once the leg is back, and `cash` computes payoutScale = 0 and reverts ZeroAmount (line 634), or pays below par, while every position is as collateralised as before. The redemption channel, which the design relies on as the peg defence, is shut or underpaying for the rest of that day because of an oracle outage that is otherwise over. Also reachable with a merely STALE Chainlink answer? No: `latestValue` still returns the old figure then, so this needs the aggregator to revert or return malformed words (deprecated/paused proxy, or sIMD's convertToAssets reverting), both cases the code explicitly handles by reading zero. Nothing is stolen; redeemers can protect themselves with minGemOut and wait. The NatSpec at lines 813-815 ('an unpriced feed counts the position for nothing') describes the write but not that it persists past recovery and feeds the lag clamp. Smallest fix: in `_resecure`, when the price is 0 leave `position.secured` and `securedCollateral` untouched (skip the re-pricing) rather than writing a zero term; alternatively re-price from the position's stored term only when a nonzero price is available.","line":819,"path":"src/CDPVault.sol","reproduction":"test/scratch/StaleLegZeroesSecured.t.sol (PASSES: it demonstrates the state). WorkBackingFixture (ParameterizedVault, $1 per collateral unit, Chainlink etched at CHAINLINK_ETH_USD). BORROWER locks 200e18 and draws 100e18; warm backing; securedCollateral == 200e18, backingPerUnit == 1e18. vm.mockCallRevert on the aggregator's latestRoundData: collateralPriceFeed.isStale() true, draw reverts StaleFeed; BORROWER wipe(1e18) succeeds and securedCollateral becomes 0. Clear the mock and refresh the answer: isStale false, collateralRatio >= 200, yet backingPerUnit() == 0 (expected: 1e18) and cash(1e18, 0, BORROWER) reverts ZeroAmount. BORROWER wipe(1e18) again: backingPerUnit still 0 (laggedNow secured == 0); only after a further BACKING_WARMUP does it read 1e18.","severity":"low","snippet":"        if (principal == 0 || price == 0) return 0;","title":"A reverting or malformed ETH/USD (or share-vault) leg reads as price 0, and a repayment or deposit made while it is down re-prices the position's securedCollateral term at zero; backingPerUnit then re"},{"citation":"resolved","description":"The parameter doc is the pre-epoch wording: the committed `_checkValue` measures every value against the epoch's ANCHOR (the value held when the epoch opened), allows STALE_DEVIATION_MULTIPLE x this plus growth once stale, and a wide epoch additionally holds later values to this cap around its first value. A reader sizing the cap from the constructor doc would conclude a 20% cap means at most 20% between consecutive accepted values, which is false in both directions (two values 40% apart are accepted in one epoch once stale; a 1% step is refused when the anchor band is spent). Fix: '@param maxDeviationBps_ The per-epoch deviation cap in bps against the epoch's anchor (see `_checkValue`, `_allowanceNow`), 0 to 10,000.'","line":156,"path":"src/SwarmFeed.sol","reproduction":"Documentation only. test/SwarmFeed.t.sol test_staleValueReanchorsOnlyWithinTheWidenedBound: with cap 1000, a value 20% above the last accepted value is accepted after a two-hour silence (so the cap is not 'the maximum change from the last accepted value'), and a value one wei above `widest` is then refused although it is within 0.01% of the last accepted value.","severity":"info","snippet":"    /// @param maxDeviationBps_ Maximum change from the last accepted value, from 0 to 10,000 bps.","title":"SwarmFeed constructor NatSpec describes maxDeviationBps as the 'maximum change from the last accepted value'; the bound is per epoch against the anchor and widens with staleness"}],"hash":"a600d2862312fefb6b4d157dfcdcbbbfdf8dec97e0e2095dff5f54ef6b558910","nodeId":"5c6c409e-09cf-4fe9-b909-d64d6436491a","outcome":"completed","summary":"The findings file holds three verified items, every quoted snippet matches its line, and the tracked tree is unchanged. Final report follows.\n\n## Result\n\nThree findings written to `.imd-findings.json`: one medium with a Foundry proof, one low with a reproduction, one info. No tracked file was changed. The scratch tests live under `test/scratch/`.\n\n| # | Severity | Where | Finding |\n|---|---|---|---|\n| 1 | medium | `src/SwarmFeed.sol:415` | The \"hour of silence\" that earns the 2x stale base is measured from the signed `issuedAt`, not from acceptance. Holding each attestation for its lifetime before relaying re-earns 40% one lifetime after every step. Proof fails on this code, passes with a two-line fix. |\n| 2 | low | `src/CDPVault.sol:819` | A reverting ETH/USD or share-vault leg reads as price 0. A `wipe` or `lock` during the outage zeroes the position's `securedCollateral` term, the lag clamp follows it down, and after recovery `backingPerUnit` reads 0 so `cash` reverts for everyone until the borrower acts, then warms up for a day. |\n| 3 | info | `src/SwarmFeed.sol:156` | Constructor NatSpec still describes the cap as the maximum change from the last accepted value. |\n\n## Answers to the numbered questions\n\n**1. Attestation acceptance.** Nothing wrong. The EIP-712 domain binds chain id and feed address, so no cross-feed or cross-chain replay. `usedRequests` is written before the question and value checks and rolls back with them. `issuedAt` must be at most one lifetime old, at or after the stored value's issue time, and not after `expiresAt`. Panel floors read the signed `panelSize` and `agreed`. The question hash is rebuilt from a pinned prefix plus the two signed window numbers, so a different question, span outside the bounds, a non-advancing window, a future window, or one older than one lifetime in blocks is refused. The attacker's only freedom is the window inside those bounds and when to relay, which is what finding 1 uses.\n\n**2. The epoch bound.** Within one lifetime of an epoch's start, N attestations move the feed at most the epoch's allowance from the anchor, and a wide epoch holds later values to the cap around its first value. After H whole hours of silence past the lifetime, one attestation may move 40% plus 2.5% per further hour, capped at 100x. Packing is exact: the bound never exceeds 1,000,000 and fits uint32, and values above 2^88 are simply not recorded as the epoch's first. The unseeded feed reads allowance 1,000,000 and `wideOpen` true, as designed. SwarmWorkOracle's override keeps only the zero check, which is right for a root. The claimed sustained rate of the cap per hour does not hold: a relayer who holds each attestation re-earns the stale base one lifetime plus the panel's signing latency after each step, so the walk compounds at 1.4x per hour and reaches 3.48x in four to five hours instead of six to seven. For the NHI feed a value relayed a day after signing allows a 95% next step. Any gap is eventually followed. There is no way to keep a wide epoch open for a later value, but the first value after a silence can be the attacker's, which the DeploymentConfig and OracleAsker comments gloss over by saying the honest value lands first.\n\n**3. OracleAsker.** Nothing wrong in the asker itself. The Treasury pays only when a keep-alive feed is near stale, when a feed has been silent a lifetime with a 60% allowance, or when an armed fall is still present five blocks later. Two pool pushes five blocks apart can trigger a paid update, as accepted, at roughly $1,100 of fees per 0.5 IMD spent. Spending is bounded by the asker's balance, which the Treasury tops up to one day's budget. In-flight slots cannot block hand relays through SwarmRelay. A timed-out request keeps `feedOf`, so its late delivery lands. The callback never reverts past the clearing, and the bound-question delivery fits under 150k of the 200k stipend in the committed gas tests. The pool slot is the Uniswap v4 `_pools` mapping at slot 6, the pri","treeHash":null,"usage":{"cachedInputTokens":4548371,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":75826,"runtime":"claude","turns":44,"wallClockMs":1334810}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"410d15d57eeec440","findings":[],"hash":"faec2fc1ecb3d5fca5aa82f7ff5e5b8b831704112fbe44abb0c326f2b795c1e8","nodeId":"144eed84-3336-4f73-9459-6161eb580fa5","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":2,"wallClockMs":214114}}],"verification":[]}