{"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":"cc8d583b-bbe6-4bcf-a0e9-3b45ea4f74af","kind":"skill:adversarial-review","nodes":[{"acceptedSubmissionHash":"18449e483e6a2096e9f7fa393f2bc1efb739acbb0ec2061b9dea6f427b937ab0","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","tools":[]},"key":"adversarial_review","kind":"code","role":"review","skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","state":"accepted"}],"objective":"Second half of the final pre-launch adversarial review of src/, script/DeployMainnet.s.sol and deploy/mainnet/ at this commit. The first half (docs/AUDIT-FINAL-2026-10-07.md: 1 high, 2 medium, 3 low) ran out of turns after six findings and never reached questions 2, 3, 6 and 8 of its request; all six findings are fixed in the commits after 002605f (git log 002605f..HEAD -- src script deploy), and an in-house review of those fixes found three more, fixed in 8dd0847 (git show 8dd0847: wideOpen stayed true after the Treasury's refresh, so anyone could make it pay every ten minutes; the one-day NHI feed widened per day, not per hour; a wide epoch stayed wide after an honest value landed). Spend your turns on the four unreached questions first, then on breaking the fixes.\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. OracleAsker.askPaid and askPaidMany: the caller pays the Intake's price per request, in-flight or duplicate feeds are skipped uncharged, one request in flight per feed until ASK_TIMEOUT, and onOracleResult (Intake-only, 200k-gas stipend) relays through SwarmRelay. Can a caller pay for a feed it did not name, be charged for a skipped one, be refunded less than owed, leave IMD stranded in the asker, block Treasury-paid asks for everyone, or make a delivery revert so a paid request never lands?\n2. Parameters.proposeWorkOracle: applies only while wage is 0; the successor must answer vault(), mintingRights() and, once anything was ever minted, predecessor() == the current oracle. Can a hostile or broken oracle be installed, can the order of oracle and wage proposals bypass the wage==0 rule, and can WORK_ORACLE_SENTINEL / WorkOracleFactory hand a vault an oracle it did not create?\n3. deploy/mainnet/bodies/: each oracle body is hash-pinned forever in OracleAsker. Confirm every window is relative, no body carries a number that moves, and the question text each feed pins (expectedQuestionHash over the window) matches its body byte for byte, including the recipe each feed's _requireQuestion expects.\n4. Known open items: work rights ignore the work feed's age; SharePriceFeed's asset-leg reads are typed calls (what a reverting or non-standard asset does to every consumer); line may be proposed below current debt (what each action does then). Say whether each is exploitable with the constants as committed.\n5. THE FIXES. (a) SwarmFeed: the per-epoch bound (every value within one lifetime measured against the epoch's anchor) whose allowance widens with staleness — twice the cap once stale, an eighth of the cap more per further whole HOUR stale beyond the lifetime (STALE_GROWTH_PERIOD, whatever the lifetime), capped at MAX_ALLOWANCE_BPS (_allowanceNow); and an epoch opened wider than the cap records its first value (_epochFirst, a uint88 packed beside _updatedAt) and holds every later value in that epoch to the cap around it as well as to the anchor's band. Show the largest move N attestations bought within one lifetime and relayed together can produce, and the largest a single attestation can produce after H hours of silence, for the one-hour price and spot feeds and the one-day NHI feed; find any sequence that re-anchors further than the allowance, any way to keep a wide epoch open for a later value, or any genuine gap that can never be followed. Check the packed uint32 bound and uint88 first value (a value above 2^88 is not recorded), the unseeded-feed case (_updatedAt 0), SwarmWorkOracle (which overrides _checkValue), and whether hourly widening makes the NHI feed cheap to re-anchor. (b) OracleAsker.wideOpen and ask: the Treasury refreshes any feed whose value is STALE and whose allowance reached WIDE_ALLOWANCE_BPS, no arming. Can that still be used to drain oracleBudget (the refresh's own epoch, an undelivered or refused request, ASK_TIMEOUT back-off), or be blocked so a feed stays wide open? (c) CDPVault.cover's dust floor: collateral worth under a millionth of the debt is swept — can a borrower lose real value, or still block cover for free? (d) DeployMainnet.verify's new checks (asker.price within ASK_MAX_PRICE, poolPrice non-zero, unseeded feeds read wide open, gap, empty work-oracle slot): what they still miss. (e) The keeper's day-one role (docs/MAINNET-RUNBOOK.md section 7): is there a state in which neither the Treasury nor a keeper following the runbook keeps NHI alive?\n6. The walk, re-costed: docs/PARAMETERS-2026-10-05.md 'The per-epoch deviation bound SHIPS' restates the ramp-and-hold attack against the epoch bound as five hours in the open at 40% per hour. Check that arithmetic against the committed constants, including the stale allowance at the epoch's open and the window rules, and state what it costs and earns at the $1M line.\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":"5c72dada2b3c74131dab22ba327adeda750cc2495f61f317c7612cbcf744eed6","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"cc8d583b-bbe6-4bcf-a0e9-3b45ea4f74af","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":"51043","feedbackHash":"775c7f572460f157160eed9d5724e2af6e03bfbc643b9a2ea7d1f7e6385f98d9","nodeKey":"adversarial_review","submissionHash":"18449e483e6a2096e9f7fa393f2bc1efb739acbb0ec2061b9dea6f427b937ab0","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"9318ee3c11df78c43c52a38141c1b7ddcddbad972aa746f124c34db4031de12a","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2565f234b0a569e9","findings":[{"citation":"resolved","description":"Q6. The doc (and the identical NatSpec at src/SwarmFeed.sol:43, 'a 3x walk takes five hours of sustained, visible manipulation') restates the ramp-and-hold walk under the per-epoch bound as 40% per hour taking five hours to reach 3.48x. The arithmetic does not follow from the committed constants. An epoch lasts exactly maxAge from the block its first value is RELAYED in (SwarmFeed._anchorAt, line 442), and a value is stale one second after maxAge from its signed issuedAt (_tooOld, line 456). The allowance of the next epoch is _allowanceNow() at the block it opens (line 389), and nothing in _checkValue/_accept carries _epochFirst or any freshness requirement across an epoch boundary. So an attacker who relays each step one hour and one second after the previous one always opens on a STALE anchor and gets STALE_DEVIATION_MULTIPLE x cap = 40% at every step, never the fresh 20%: the value compounds at 1.4 per hour, 1.96x at 1h+1s, 2.744x at 2h+2s, 3.84x at 3h+3s, and 3.48x is passed by the fourth attestation about three hours after the first. 'Five hours' is the 1.4 x 1.2^5 = 3.484 chain of the OLD last-value bound, which assumed every later step is measured against a still-fresh value; under the epoch bound the attacker chooses the timing and the fresh 20% step never has to happen. Two further things the figure omits. (1) The first step's allowance is whatever the silence bought (_allowanceNow: 4000 + 250 x whole hours past the lifetime). Price feeds are not keep-alive, a rise never arms (DRIFT_RISE_TRIGGER_OF_CAP_BPS = 0) and wideOpen needs 60%, so for eight hours and fifty-nine minutes of silence nothing refreshes them and the attacker takes 57.5%: 1.575 x 1.4 = 2.2x at 1h+1s, 3.09x at 2h+2s. An attacker who instead waits for wideOpen (9h) can front-run the Treasury's honest delivery with their own 60% value, which _epochFirst then protects for the hour (the honest 1.0A is refused, ExcessDeviation, Treasury back-off 2h): 1.6 x 1.4^3 = 4.39x at 3h+3s. (2) The window rules do not slow it: steps are 300 blocks apart, the primary's 600-block window has 7 of its 13 samples in its newer half, so a pool held at each rung for one hour gives every window its median at the rung and the spot (last block) the same figure, within SKEW_BPS of each other, from ONE ramp-and-hold round trip. Cost and earnings at the committed constants (LINE $1M, mat 170 at NHI >= 0.85; pool 841 ETH / 207,881 IMD at IMD $10.92, ETH $2,699, 1% fee, the doc's own inputs): pushing a constant-product price by M needs 841 x (sqrt(M) - 1) ETH in and the same out, so 3.48x: 728 ETH ($1.97M), ~$39k round trip; 3.84x: 807 ETH, ~$44k; 4.39x: 921 ETH, ~$50k. The vault lends $1M against collateral worth $1.7M / M at market: $489k at 3.48x (gain $511k), $443k at 3.84x ($557k), $387k at 4.39x ($613k). Net roughly $470k-$560k for about three hours (two with a 57.5% start) of holding a $2.3M-a-side pool pumped, the attacker's exposure to holders selling into it being the only remaining cost. The revisit rule in the same section says the epoch bound 'keeps the answer five hours in the open'; the constants make it about three. The paragraph also re-costs only the borrowing direction: for the liquidation direction (docs/AUDIT-INTERNAL-2026-10-06.md finding 1) the grace is already six hours at NHI >= 0.85, so a 40%-per-hour bound adds no delay there at all (0.6A at bark, 0.6 x 0.6^k an hour later each step, bounded only by the IMD the attacker can sell, with honest attestations refused for 11+ hours afterwards because the allowance back UP to market grows only 2.5%/h). Not a code defect: the constants are a deliberate choice, but the stated safety condition for proposeLine is wrong by nearly a factor of two in time, and no committed test exercises more than one epoch (test_chainedAttestationsCannotWalkPastTheEpochBound). Smallest fix: restate as 40% per hour compounding (1.4^n: 3.84x in three hours, 3.48x on the fourth attestation), first step up to 57.5% (60% racing the Treasury)","line":185,"path":"docs/PARAMETERS-2026-10-05.md","reproduction":"test/scratch/WalkRate.t.sol (PASSES on this code: it is the demonstration). SwarmFeed with maxAge 1 hours, cap 2000, seeded V = 3.55e15 at T0. warp T0+1h+1s. Four times: epoch() reports allowanceBps 4000 (expected per the doc after the first step: 2000); _accept(V x 1.4^k, now) is accepted; warp 1h+1s. Actual: latestValue 3.8416 V at T0+4h+4s, i.e. accepted at T0+3h+3s; 3.48x passed on the fourth attestation. Second test: warp T0+8h59m: epoch() allowanceBps 5750; 1.575V accepted; 1h+1s later 2.205V accepted (2.2x); 1h+1s later 3.087V accepted (3.08x in two hours and two seconds). python3: for M in (3.48,3.84,4.39): print(M, 841*(M**0.5-1), 841*(M**0.5-1)*2699*0.02, 1e6-1.7e6/M).","severity":"medium","snippet":"per-epoch bound makes that walk 40% per hour, so 3.48x takes five hours of visible manipulation.","title":"PARAMETERS 'per-epoch bound SHIPS': the walk is 40% per hour COMPOUNDING (every step opens on a stale anchor), so 3.48x takes about three hours, not five; first step up to 57.5-60%; ~$40-50k of fees a"},{"citation":"resolved","description":"5(b), the fix in 8dd0847. wideOpen requires the feed's value to be STALE and reads the allowance from SwarmFeed.epoch(), which during a live epoch is the STORED bound of that epoch. Staleness is measured from the attestation's signed issuedAt (SwarmFeed._updatedAt = issuedAt, _tooOld), but the epoch is measured from the block the attestation was RELAYED in (_anchorAt = block.timestamp). Inside the wide epoch an honest refresh opens, the value therefore goes stale at issuedAt + maxAge, BEFORE the epoch expires at relayedAt + maxAge, and for that gap wideOpen is true again: isStale() true, epoch() still reporting the >= 6000 bound the refresh opened. Anyone calls ask(feed, body) and the Treasury pays 0.5 IMD for a second refresh of a feed refreshed less than an hour ago (ASK_MIN_INTERVAL long passed). The gap equals the delivery latency of the Intake's callback (panel signature to writer callback, minutes), and up to a whole lifetime for an answer bought off chain and relayed by hand (SwarmFeed accepts issuedAt up to maxAge old and the window up to 300 blocks old). The NatSpec at lines 298-302 claims the property the code does not have: 'Once a value has landed it is fresh, and the feed is not wide open again until it has been silent long enough to be.' Bounded: one extra Treasury purchase per wide refresh (the second delivery lands within first +- 20%, is accepted, and the value is fresh again), about 1.3 extra IMD a day for two price feeds in a dead market, inside the 15 IMD budget; the adversarial variant costs the attacker 0.5 IMD per 0.5 IMD of Treasury spend plus the off-chain purchase. So a leak, not a drain, but it is the exact behaviour 8dd0847 set out to close and the regression test (test_aDeliveredRefreshClosesWideOpen) only checks the block of delivery with issuedAt == block.timestamp. Reachable with the constants as committed. Smallest fix: also require that no epoch is live, which is what 'silent long enough' means: `(, uint64 openedAt, uint256 allowance) = SwarmFeed(feed).epoch(); return SwarmFeed(feed).isStale() && openedAt == block.timestamp && allowance >= WIDE_ALLOWANCE_BPS;` (epoch() reports openedAt == block.timestamp exactly when the stored epoch has run its maxAge; unseeded feeds on mainnet still read wide open, so DeployMainnet.verify line 323 still passes).","line":304,"path":"src/OracleAsker.sol","reproduction":"test/scratch/WideOpenReopensInsideLiveEpoch.t.sol (FAILS on this code). Price-policy feed (maxAge 1h, cap 2000) seeded V at T0; OracleAsker funded 15 IMD; warp T0+9h+1s: wideOpen true, ask() pays 0.5 IMD; the Intake delivers an attestation with issuedAt = now - 5 minutes: accepted, wideOpen false. warp 55 min + 1 s: feed.isStale() == true, epoch() = (anchor V, openedAt 55 min ago, allowance >= 6000). Expected (NatSpec 298-302): wideOpen false and ask() reverts NotArmed, asker balance unchanged. Actual: wideOpen true, ask() succeeds and the asker pays another 0.5 IMD. Second test: feed stale 9h; anyone relays by hand a valid attestation with issuedAt = now - 59 minutes; 1 min + 1 s later wideOpen is true for the remaining 59 minutes of that epoch and ask() pays.","severity":"low","snippet":"        if (!SwarmFeed(feed).isStale()) return false;","title":"OracleAsker.wideOpen is true again INSIDE the live wide epoch an honest refresh opened, once the value's issuedAt is a lifetime old, so the Treasury pays a second time per silence (and a hand-relayed "},{"citation":"resolved","description":"Q1 ('make a delivery revert so a paid request never lands'). When a request has been in flight for ASK_TIMEOUT (2 hours) and anyone asks again for the same feed (ask or askPaid), _request deletes feedOf[f.inFlight] before recording the new one. If the Intake then delivers the OLD request, onOracleResult hits `if (feed == address(0)) revert UnknownRequest(requestId)` at line 258 and reverts. The Intake records a failed callback for a request the Treasury or an askPaid caller paid 0.5 IMD for, and the attestation is never relayed by the asker, contrary to the function's own contract (lines 265-268: the callback 'never reverts' on a delivery it cannot land, 'clears the feed's in-flight slot either way and reports whether it relayed'; 'An answer it could not deliver stays public, and anyone can relay it by hand' presumes the Intake exposes it after a reverted callback). Whether the paid answer is lost for good depends on the Intake's handling of a reverting callback, which this repository cannot see; from the asker's side it is refused. The delete is also unnecessary: the `if (f.inFlight == requestId)` guard at line 261 already keeps a superseded delivery from touching the live slot, so a late answer could be relayed harmlessly (SwarmFeed refuses it itself if its window no longer advances). Reachable with the constants as committed whenever the swarm takes more than two hours to answer (an undelivered request is exactly the case ASK_TIMEOUT exists for), and anyone can be the second asker. No funds at risk beyond the price of the request. Smallest fix: remove the `delete feedOf[f.inFlight]` from _request; onOracleResult already deletes feedOf[requestId] on delivery and only clears the slot when the id matches.","line":237,"path":"src/OracleAsker.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\nimport {SwarmRelay} from \"src/SwarmRelay.sol\";\nimport {OracleAsker} from \"src/OracleAsker.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {APPROVED_OPERATOR, INTAKE, ORACLE_ACTION, ATTESTATION_RELAYER, ASK_TIMEOUT} from \"src/DeploymentConfig.sol\";\n\ncontract HourFeed2 is SwarmFeed {\n    constructor(address attester_) SwarmFeed(attester_, ATTESTATION_RELAYER, 1, 3, 1 hours, 2_000) {}\n\n    function seed(uint256 value) external {\n        _accept(value, uint64(block.timestamp));\n    }\n}\n\ncontract ScratchIntake2 {\n    struct Callback {\n        address target;\n        bytes4 selector;\n    }\n\n    mapping(bytes32 => mapping(address => uint256)) public priceOf;\n    mapping(bytes32 => Callback) public callbackOf;\n    uint256 public nonce;\n    bytes public lastRevert;\n\n    function setPrice(bytes32 action, address asset, uint256 amount) external {\n        priceOf[action][asset] = amount;\n    }\n\n    function request(bytes32 action, bytes calldata, Callback calldata callback, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 requestId)\n    {\n        require(priceOf[action][asset] != 0 && amount >= priceOf[action][asset], \"not sold\");\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        requestId = keccak256(abi.encode(address(this), ++nonce));\n        callbackOf[requestId] = callback;\n    }\n\n    function complete(bytes32 requestId, bytes calldata args) external returns (bool delivered) {\n        Callback memory c = callbackOf[requestId];\n        bytes memory ret;\n        (delivered, ret) = c.target.call{gas: 200_000}(bytes.concat(c.selector, args));\n        lastRevert = ret;\n    }\n}\n\n/// @notice FINDING (low). `OracleAsker._request` deletes `feedOf[f.inFlight]` when a new request replaces\n/// one that has passed ASK_TIMEOUT, so when the Intake later delivers the OLD request — paid for by the\n/// Treasury or by an askPaid caller — `onOracleResult` reverts `UnknownRequest` and the answer is never\n/// relayed. The function's own contract is \"never revert past this point ... An answer it could not\n/// deliver stays public, and anyone can relay it by hand\"; here it reverts before that point, and the\n/// Intake records a failed callback for a request someone paid 0.5 IMD for. The `f.inFlight == requestId`\n/// check already handles a late delivery safely, so the delete buys nothing.\n///\n/// Fails on the committed code (the late callback reverts); passes once `_request` stops deleting the\n/// superseded request's `feedOf` entry.\ncontract LateDeliveryDroppedTest is Test {\n    uint256 private constant ATTESTER_KEY = 0xA11CE;\n    uint256 private constant PRICE = 0.5 ether;\n    uint256 private constant V = 3_550_000 gwei;\n    bytes private constant BODY = '{\"question\":\"IMD/ETH median\"}';\n    address private constant PAYER = address(0xFACE);\n\n    MockIMD private imd;\n    ScratchIntake2 private intake;\n    HourFeed2 private feed;\n    OracleAsker private asker;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.warp(100 days);\n        vm.roll(1_000_000);\n        vm.etch(ATTESTATION_RELAYER, address(new SwarmRelay()).code);\n        vm.etch(INTAKE, address(new ScratchIntake2()).code);\n        intake = ScratchIntake2(INTAKE);\n        imd = new MockIMD();\n        intake.setPrice(ORACLE_ACTION, address(imd), PRICE);\n        feed = new HourFeed2(vm.addr(ATTESTER_KEY));\n        address[] memory feeds = new address[](1);\n        feeds[0] = address(feed);\n        bytes32[] memory hashes = new bytes32[](1);\n        hashes[0] = keccak256(BODY);\n        bool[] memory tracks = new bool[](1);\n        tracks[0] = true;\n        bool[] memory keepAlive = new bool[](1);\n        asker = new OracleAsker(IERC20(address(imd)), feeds, hashes, tracks, keepAlive);\n        vm.prank(APPROVED_OPERATOR);\n        imd.mint(PAYER, 10 ether);\n        feed.seed(V);\n    }\n\n    function test_aLateAnswerToASupersededRequestIsStillRelayed() public {\n        vm.startPrank(PAYER);\n        imd.approve(address(asker), type(uint256).max);\n        bytes32 first = asker.askPaid(address(feed), BODY, PRICE);\n        // The panel is slow: nothing is delivered for ASK_TIMEOUT, so the same payer (or anyone) asks again.\n        vm.warp(block.timestamp + ASK_TIMEOUT);\n        vm.roll(block.number + ASK_TIMEOUT / 12);\n        bytes32 second = asker.askPaid(address(feed), BODY, PRICE);\n        vm.stopPrank();\n        assertTrue(first != second);\n        assertEq(asker.feedOf(first), address(0), \"the superseded request was forgotten\");\n\n        // The Intake now delivers the FIRST answer, signed a moment ago (a valid, fresh attestation).\n        SwarmFeed.OracleAttestation memory a = _attestation(keccak256(\"late\"), V * 101 / 100);\n        bool delivered = intake.complete(first, abi.encode(first, a, _sign(a)));\n\n        // EXPECTED: the callback completes (it promises never to revert once the request is known to be\n        // this asker's) and the paid-for answer lands in the feed.\n        assertEq(bytes4(intake.lastRevert()), bytes4(0), \"callback reverted (UnknownRequest)\");\n        assertTrue(delivered, \"the Intake recorded a failed callback for a paid request\");\n        (uint256 value,) = feed.latestValue();\n        assertEq(value, V * 101 / 100, \"the answer PAYER paid 0.5 IMD for never landed\");\n    }\n\n    function _attestation(bytes32 id, uint256 figure) private view returns (SwarmFeed.OracleAttestation memory a) {\n        a.requestId = id;\n        a.chainId = 1;\n        a.questionHash = keccak256(\"q\");\n        a.answerType = 3;\n        a.answer = abi.encode(figure);\n        a.figure = figure;\n        a.fromBlock = uint64(block.number - 600);\n        a.toBlock = uint64(block.number);\n        a.blockHash = keccak256(\"b\");\n        a.panelJobId = keccak256(\"panel\");\n        a.panelSize = 60;\n        a.quorum = 20;\n        a.agreed = 40;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 1 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/LateDeliveryDropped.t.sol (FAILS on this code). PAYER approves and calls askPaid(feed, body, 0.5e18) -> R1; warp ASK_TIMEOUT (2h); askPaid again -> R2; asker.feedOf(R1) == address(0). The Intake now completes R1 with a valid, freshly signed attestation (figure 1.01 V). Expected: the callback completes (Delivered event, relayed true) and feed.latestValue() == 1.01 V. Actual: the callback reverts with selector 0x7bbe34c9 (UnknownRequest(bytes32)), the Intake records delivered == false, and the feed still holds V.","severity":"low","snippet":"        if (f.inFlight != bytes32(0)) delete feedOf[f.inFlight]; // a timed-out request no longer counts","title":"OracleAsker._request deletes feedOf of a timed-out request, so the Intake's late delivery of that paid request reverts UnknownRequest and the answer is never relayed"},{"citation":"resolved","description":"5(c), the fix in cc4103f for the final review's low finding. _coverDust (line 577) sweeps collateral below the seizure for max(debt / COVER_DUST_DIVISOR, 1 wei), so a borrower who re-locks exactly that many raw units plus a rounding margin is above the floor and cover reverts NoRealizedBadDebt at this line. That collateral is worth 1.2e-6 of the position's debt: $0.0012 on a $1,000 bad debt, $1.20 on $1M, and in sIMD raw units (24 decimals) a number anyone holds. Nothing then reaches it except bite, which needs a NEW mark (the drain's mark has expired, that is when the griefer acts), the full grace (lull(): six hours at NHI >= 0.85) and a liquidator who sizes debtToRepay to the exact raw amount (a one-wei bite leaves a remainder that is not below _oneWeiSeizure and so is not swept; an oversized bite reverts InsufficientCollateral). The borrower then re-locks for the next cycle. So the griefing the finding described remains with its cost raised from ~1e-20 sIMD to 1.2 millionths of the debt plus one lock transaction per ~7 hours: still gas-dominated, still free in capital. Harm as before: the Treasury's imdUSD equal to the record stays locked behind BadDebtFirst (Treasury.withdraw, payStream) and totalBadDebt keeps growing with fees while the record cannot be retired. No funds move to the borrower; griefing only. Reachable with the constants as committed. The NatSpec at lines 572-576 ('Collateral worth under a millionth of the debt is dust in every economic sense') is true but the closure it implies ('could re-lock for free') does not hold one unit above the floor. Smallest fix, either: let bite skip mark-and-grace for a position whose _recordedBadDebt is nonzero (it has already been drained once, and the grace exists to let a borrower who could recover react), or raise the floor to the larger of a millionth of the debt and an absolute value (one imdUSD of collateral at `price`), so that re-locking enough to block cover costs real money.","line":531,"path":"src/CDPVault.sol","reproduction":"test/scratch/CoverDustStillBlocks.t.sol (PASSES: it demonstrates the state). WorkBackingFixture, price $1 per 1e18 raw. BORROWER locks 170e18, draws 100e18; KEEPER locks 450e18, draws 250e18; price to $0.50; bark; +6h; bite(70.83e18) drains BORROWER (bad debt ~29e18 + fees); price back to $1; Treasury covers half; warp past the mark's tail (+1 day +1s). BORROWER locks floor_ = mulDiv(remaining/1e6, 1.2e18, price) x 1.001 raw units, worth under $0.00002. Expected (fix intent): cover(BORROWER, remaining) sweeps the dust and retires the debt. Actual: cover reverts NoRealizedBadDebt; bite(BORROWER, 1) reverts MarkExpired; after bark, bite reverts GracePeriodNotElapsed for six hours; then bite(BORROWER, 1) leaves a remainder and cover still reverts NoRealizedBadDebt; only bite(BORROWER, held x price / 1.2e18) sweeps it (one rounding unit may remain) and cover succeeds; the borrower can repeat.","severity":"low","snippet":"            if (position.collateral >= _coverDust(owner, price)) revert NoRealizedBadDebt();","title":"cover's new dust floor still lets a drained borrower block cover for gas: re-locking collateral worth 1.2 millionths of the debt (about $0.00002 here) forces a fresh mark, six hours of grace and an ex"},{"citation":"resolved","description":"5(e). On day one the keeper is NHI's only buyer: Treasury.fundOracle pays only from sIMD the Treasury holds (none until the first liquidation cut), and the runbook has the keeper buy 'NHI near stale first' with its own IMD inside ASK_PAID_IMD_PER_DAY (2 IMD, i.e. four purchases). The state in which neither keeps NHI alive: the figure api.imd.fun/swarm now reports lies outside the feed's allowance. NhiFeed carries the same 2,000 bps cap as the price feeds, bounded per one-day epoch against the anchor, and the NHI question is a live API read with no intermediate values (SwarmFeed._checkValue, _allowanceNow). A genuine move of more than 20% in a day (one core service down is -10 points on a 1.0 index, agentsOnline halving is -20, a batch of blocked jobs moves r) is refused with ExcessDeviation by EVERY purchase until the stored value is a day stale: allowance 2000 until 24h, 4000 at 24h+1s, 4250 at 25h, 5000 at 28h. A refused answer still costs the full Intake price (OracleAsker.askPaid pulls it before the request; the back-off in onOracleResult applies only to the Treasury's ask). A keeper that follows the runbook and retries its refused NHI purchases spends its 2 IMD on four refusals between the 18-hour near-stale mark and 24 hours, and has nothing left at 24h+1s when the first purchase that WOULD be accepted becomes possible; the Treasury has no sIMD to help, and its own ask path would back off two hours per refusal anyway. NhiFeed.isStale() turns true at 24h and ParameterizedVault._pricingStale() then refuses draw, free with debt, bark, bite, cash and cover's dust path for the rest of the keeper's UTC day (or until someone else pays 0.5 IMD with the allowance open). The runbook tells the keeper neither to read SwarmFeed.epoch() before buying (the allowance is public exactly so a buyer can see 'how far the feed will follow') nor to keep budget for the accepting attempt. Reachable with the constants as committed; it needs a real NHI move, which is what the feed exists to report. Smallest fix: a sentence in 7.4 that the keeper reads epoch() and the live API figure and does not buy an NHI answer the feed will refuse, holding its last purchase for the first second the allowance covers the gap; and seed the asker with IMD at deploy (the earlier finding's fix) so the Treasury path is not dead on day one.","line":334,"path":"docs/MAINNET-RUNBOOK.md","reproduction":"On-chain state: NhiFeed value 0.95e18, accepted at T0 (anchor, epoch opened T0, cap 2000). The control plane now reports 0.70e18 (-26.3%). Every submitAttestation(figure 0.70e18) through SwarmRelay reverts ExcessDeviation while block.timestamp <= T0 + 24h (allowance 2000: |0.70-0.95| = 0.25 > 0.19) and is accepted from T0 + 24h + 1s (allowance 4000: 0.25 <= 0.38). A keeper following 7.4: askPaid(nhi) at T0+18h (near stale) refused; retries at +20h, +22h, +23h59m refused; 2 IMD spent (ASK_PAID_IMD_PER_DAY). Treasury.fundOracle() returns 0 (maxWithdraw 0). At T0+24h+1s NhiFeed.isStale() == true and ParameterizedVault.draw(1) / bark / bite / cash revert StaleFeed. Expected per section 7.4: 'The keeper covers that gap'. Actual: NHI stale until the keeper's next UTC day or another buyer; the first purchase after T0+24h+1s is accepted.","severity":"low","snippet":"   (`KEEPER_ORACLE_FALLBACK`, within `ASK_PAID_IMD_PER_DAY`): NHI near stale first, then falls, then a","title":"Runbook section 7.4: when the honest NHI figure is more than 20% from the feed's anchor, every keeper purchase is refused and still charged, so neither the Treasury (no sIMD) nor a keeper on its 2 IMD"},{"citation":"resolved","description":"Q2. proposeWorkOracle (NatSpec lines 239-243) requires, once vault.totalEarned() != 0, that the successor 'name the current oracle as its predecessor'. IWorkOracleSuccessor.predecessor() is a typed call; SwarmWorkOracle, the only attested work oracle in the repository and the one WORK_ORACLE_FACTORY creates, exposes no such function, so the call reverts and the proposal is refused for every SwarmWorkOracle, including one deployed fresh against this vault. The documented path to 'integrating minting from work upstream never forces a new vault' therefore requires a contract that does not exist yet, and the fallback of proposing address(0) is also refused once minted. Not exploitable and not a bypass: wage == 0 is checked at proposal and application, a single Governed slot means a wage and an oracle change cannot be pending together, WorkOracleFactory.create can only return an oracle whose vault is its caller, and the sentinel path reverts rather than downgrading. The ordering of wage and oracle proposals cannot bypass wage == 0; rights claimed but never consumed in the old oracle are stranded when it is replaced (they are re-claimable in a successor with an empty creditedTasks only while totalEarned is zero), which is a governance choice visible for 48 hours. Smallest fix: add `address public immutable predecessor` (zero for a created oracle) to SwarmWorkOracle and a constructor argument on the factory's second path, or document in Parameters that the successor must be a new contract type.","line":390,"path":"src/Parameters.sol","reproduction":"test/scratch/SuccessorHasNoPredecessor.t.sol (PASSES: demonstration). Wage 1e18 applied, reserve listed, WORKER earns 1e18 (totalEarned != 0), wage back to 0 applied. `new SwarmWorkOracle(address(vault), 1 days)` (vault() == vault). Expected per NatSpec: proposable if it names the current oracle as predecessor. Actual: proposeWorkOracle(successor) reverts (empty revert data: predecessor() is not a function of SwarmWorkOracle).","severity":"info","snippet":"            if (minted && successor.predecessor() != vault.oracle()) revert InvalidWorkOracle();","title":"SwarmWorkOracle has no predecessor(), so once anything was minted from work no shipped work oracle can ever be proposed as a successor: Parameters._validate reverts in the typed call"},{"citation":"resolved","description":"5(d). The new check proves the slot keccak256(IMD_POOL_ID, 6) holds a nonzero sqrtPriceX96, i.e. that IMD_POOL_ID is SOME initialised pool on POOL_MANAGER. The bodies (and the feeds' pinned questions) name pool 0xb07d...fbf3 in text; OracleAsker.driftBps compares the feeds' value against whatever IMD_POOL_ID reads. With IMD_POOL_ID pointing at any other initialised pool the script prints 'Deployed and verified' and the Treasury's fall trigger compares IMD against an unrelated price forever: it pays for falls that did not happen (bounded by the budget and the arming rule) or never pays for ones that did (collateral over-valued for up to an hour each time). Other reads still not made by verify: the work oracle's expectedQuestionHash(1,2) != 0 and its attestationChainId/answerType (checked for the three feeds, not for the fourth SwarmFeed); and the Chainlink leg, which _refuseUnlessReady checks before a broadcast but verify does not, so `--sig verify(...)` against an existing deployment does not notice a dead ETH/USD aggregator (UsdPriceFeed.isStale() true, every price action StaleFeed). The asker-balance / Treasury-sIMD day-one check the earlier finding asked for is also absent (the runbook moved it to the keeper). Smallest fix: require(vm.contains(string(p.priceBody), vm.toString(IMD_POOL_ID)) && vm.contains(string(p.spotBody), vm.toString(IMD_POOL_ID))), require(work.expectedQuestionHash(1, 2) != bytes32(0)), and call _preflightPriceLeg() from verify.","line":322,"path":"script/DeployMainnet.s.sol","reproduction":"Fork of mainnet with src/DeploymentConfig.sol IMD_POOL_ID edited to the id of any other initialised v4 pool (e.g. the ETH/USDC pool on 0x000000000004444c5dc75cB358380D2e3dE08A90); run deploy/mainnet/rehearse-fork.sh or `forge script script/DeployMainnet.s.sol --sig run()`. Expected: verification fails naming the pool mismatch. Actual: asker.poolPrice() is nonzero, 'Deployed and verified' is printed, and OracleAsker.driftBps(priceFeed) reports the gap between IMD's feed and the other pool's price.","severity":"info","snippet":"        require(asker.poolPrice() != 0, \"asker: the pool slot reads empty (POOL_MANAGER or IMD_POOL_ID wrong)\");","title":"DeployMainnet.verify checks poolPrice() != 0 but not that IMD_POOL_ID is the pool the pinned bodies name, so drift against a different initialised pool passes verification"}],"hash":"18449e483e6a2096e9f7fa393f2bc1efb739acbb0ec2061b9dea6f427b937ab0","nodeId":"2ad238f0-b100-49f6-99f9-8c47239544bd","outcome":"completed","summary":"Review complete. No source, script, config or doc file was changed; the only additions are `.imd-findings.json` and five scratch tests under `test/scratch/`.\n\n## Result\n\nSeven findings, none above medium. The three fixes in 8dd0847 hold in substance. One has a residual gap (wideOpen can re-trigger inside the live epoch), the dust floor still leaves the re-lock griefing free in capital terms, and the walk arithmetic the parameters doc attaches to `line` is wrong by nearly a factor of two in time. Three scratch tests fail on the committed code and are embedded as proofs or reproductions; two more pass as demonstrations of a state.\n\n| # | Severity | Where | Finding |\n|---|---|---|---|\n| 1 | medium | docs/PARAMETERS-2026-10-05.md:185, src/SwarmFeed.sol:43 | The walk is 40% per hour compounding, since every step can open on a stale anchor. 3.84x in 3h+3s, 3.48x on the fourth attestation, not five hours. First step up to 57.5% unopposed, 60% racing the Treasury. |\n| 2 | low | src/OracleAsker.sol:304 | wideOpen is true again inside the live wide epoch once the value's issuedAt is a lifetime old, because staleness runs from issuedAt and the epoch from relay time. One extra Treasury purchase per refresh; a hand-relayed answer held 59 minutes reopens it for 59 minutes. Test fails on this code. |\n| 3 | low | src/OracleAsker.sol:237 | A request that times out and is replaced has its feedOf deleted, so the Intake's late delivery reverts UnknownRequest and the paid answer is never relayed. Test fails on this code. |\n| 4 | low | src/CDPVault.sol:531 | Re-locking collateral worth 1.2 millionths of the debt still blocks cover for a mark, six hours of grace and an exactly-sized bite. Free in capital, gas only. |\n| 5 | low | docs/MAINNET-RUNBOOK.md:334 | When the live NHI figure is more than 20% from the anchor, every keeper purchase is refused and charged; the 2 IMD cap is gone before the allowance opens at 24h+1s and the vault halts. |\n| 6 | info | src/Parameters.sol:390 | SwarmWorkOracle has no predecessor(), so after any mint no shipped work oracle can be a successor. |\n| 7 | info | script/DeployMainnet.s.sol:322 | verify proves IMD_POOL_ID is some initialised pool, not the pool the bodies name. |\n\n## Answers to the numbered questions\n\n**1. askPaid and askPaidMany.** A caller cannot pay for a feed it did not name, be charged for a skipped feed, or be refunded less than owed: the price is read once, pulled per bought request, and skipped or duplicate feeds `continue` before the transfer. IMD is stranded in the asker only if the Intake pulls less than the approved amount, which this repository cannot see. Treasury-paid asks for a feed can be held off for two hours by anyone who hand-relays the panel's answer before the Intake's callback, because the catch at line 278 applies the refusal back-off to duplicates too; harmless, since the feed was refreshed. The one way a paid delivery is dropped is finding 3.\n\n**2. proposeWorkOracle.** The wage rule cannot be bypassed by ordering: `_validate` runs at proposal and application, and a single Governed slot means a wage change and an oracle change are never pending together. A hostile oracle can only be installed by the governor, within the documented trust. The sentinel path reverts on an absent factory, `WorkOracleFactory.create` can only return an oracle whose vault is its caller, and `_validateOracle` refuses anything naming another vault. The gap is finding 6: the successor path is unimplementable with the shipped contract.\n\n**3. Bodies.** Nothing wrong. All three windows are relative. The only numbers are the pool id, PoolManager, token address, panel floors, tolerance and validity, none of which move. `check-bodies.mjs` passes here, and the three pinned prefixes decode to the same recipe text each body carries, with `evidence` present only for the NHI panel question. The window spans the service will resolve fit each feed's bounds. The pinned prefixes have still never been checked against a live","treeHash":null,"usage":{"cachedInputTokens":5748426,"inputTokens":770,"model":"claude-fable-5-1","outputTokens":101844,"runtime":"claude","turns":56,"wallClockMs":1462708}}],"verification":[]}