{"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":"d744e818-03e4-42ad-9cea-192ec55f49d6","kind":"skill:adversarial-review","nodes":[{"acceptedSubmissionHash":"077db6979ab4dbfb2cef70c3b0d5101426e2bc80b1be651087f7e7fe2010253c","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":"Final pre-launch adversarial review of src/, script/DeployMainnet.s.sol and deploy/mainnet/ at this commit. Two audit rounds and their fixes are already in (docs/AUDIT-FIX-PLAN-2026-10-05.md, docs/AUDIT-INTERNAL-2026-10-06.md); weight your time to what changed since 03e8d0c9ed18204a041e48141bbff44b48da226b (git diff 03e8d0c..HEAD -- src script/DeployMainnet.s.sol) and to the deployment, which no audit has seen.\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. SwarmFeed deviation bound (ce39fc6): 20% from the last accepted value, 40% once that value is older than maxAge, never lifted. Show the largest move N attestations bought within one hour can produce when relayed together, given the window rules (span bounds, toBlock advancing, toBlock within maxAge/12 blocks of head). docs/PARAMETERS-2026-10-05.md section 'No rolling bound' argues this is uneconomic at line $1M: check its arithmetic against the constants as committed and the 13-sample median in PriceFeed's question, and state the line at which it stops holding.\n2. OracleAsker (84182f0, 73c1163): ask pays only on a FALL of DRIFT_FALL_TRIGGER_OF_CAP_BPS of the cap, armed ARM_DELAY_BLOCKS earlier and still present, or for NHI keep-alive at STALE_AT_BPS; askPaid and askPaidMany charge the caller per request, skip in-flight or duplicate feeds uncharged, and back off ASK_TIMEOUT after an undelivered result. Can anyone make the Treasury pay when the chain does not justify it, exceed oracleBudget in a UTC day, get a request paid without a delivery being possible, leave IMD stranded in the asker, or block updates for everyone? Can a caller of askPaidMany pay for a feed it did not name or be refunded less than it is owed?\n3. Parameters.proposeWorkOracle (84182f0): 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 the sentinel/factory path (WORK_ORACLE_SENTINEL, WorkOracleFactory) be made to hand a vault an oracle it did not create?\n4. The phase-2 fixes (9dd2149): redemption cap = min(live, lagged backing) with same-transaction and fresh capital removed from both sides; cover's fee remint reaching totalReceived; the USD reserve result capped at 1e36; the lock(1) freeze. Find a sequence across adjacent blocks or transactions that still pays a redeemer above backing, or desynchronises totalReceived or totalBadDebt.\n5. DeployMainnet.s.sol and deploy/mainnet/plan.py: CREATE2 through the canonical deployer in layers, refusing to broadcast unless every source constant equals its planned address. What happens if a constructor reverts mid-broadcast (which contracts exist, which trust an address that does not), whether any contract reads another's state in its constructor before that contract is deployed, whether salts collide or can be front-run, whether the 0.05 ETH gas ceiling can leave a half-deployed stack, and what the readback after the broadcast fails to check. The Intake (INTAKE) is deployed by a third party before us: list every assumption the stack makes about it.\n6. 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) matches its body.\n7. Constants: every value in src/DeploymentConfig.sol and the deploy script against the Decisions and Final confirmation sections of docs/PARAMETERS-2026-10-05.md. Report any mismatch, any constant the script does not verify after deploy, and any Sepolia-only address that plan.py does not replace.\n8. Known open items from the reference docs: work rights ignore the work feed's age; SharePriceFeed's asset-leg reads are typed calls (what a reverting or non-standard asset does); line may be proposed below current debt (what each action does then). Say whether each is exploitable with the constants as committed.\n9. The seams: vault and Treasury (fees, cover, payStream, reserve protection, fundOracle's share unwrap), vault and feeds (1-hour lifetimes, SKEW_BPS, 24-decimal collateral), and the relay bundles (relayAndBark, relayAndBite). Anything that one audit panel's scope ended at and another's did not begin.\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":"06ad0ef28d9c987d49c35625c7b8f9838dc2f4078a290fa52acb4745ecc348f8","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"d744e818-03e4-42ad-9cea-192ec55f49d6","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":"52207","feedbackHash":"35818f53da56274ea3cdacf33ab5a54bda1835833443ef45d349845f61493cd0","nodeKey":"adversarial_review","submissionHash":"077db6979ab4dbfb2cef70c3b0d5101426e2bc80b1be651087f7e7fe2010253c","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"4cc19eb735dcf9c7df87bfd1d5dbad441ca01148fc87c60c21d66966deb6c2bf","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f009de0ce52c7b3a","findings":[{"citation":"resolved","description":"Q1/Q9. ce39fc6 replaced the lifted stale bound with STALE_DEVIATION_MULTIPLE x cap (40% at the launch cap of 2,000 bps), and the bound never widens further however long the feed has been stale. The pinned PriceFeed question is 'the median of 13 evenly spaced samples' of IMD's v4 pool and the SpotFeed question is the window's last block, and toBlock must be within maxAge/12 = 300 blocks of head. After a gap move of more than 40% (IMD moved 44.6% in one day, docs/MAINNET-RUNBOOK.md section 7.1; the parameters doc counts a -30.84% single trade as the observed worst day and the pool thins after each sale) every honest answer the swarm can sign is the post-move price: a step function sampled at any 13 blocks has a median equal to one of its two levels, and the spot reads one block. So every attestation reverts ExcessDeviation, for ever, unless the market itself returns to within 40% of the stale value. Both feeds are immutable, the vault pins them, and _requireFreshFeeds gates draw, free-with-debt, bark, bite, cash and cover's dust path: a healthy +50% rally in a quiet week (nobody pays for a rise: DRIFT_RISE_TRIGGER_OF_CAP_BPS = 0, the keeper never buys one) or a crash that two large sells produce freezes the whole protocol with no admin path and no redeploy path short of a new vault. In the fall case the Treasury also keeps paying: pool below feed by >5% arms, ask is bought every ASK_TIMEOUT after each refused delivery, 6 IMD a day per feed until the budget is gone. The NatSpec at src/SwarmFeed.sol:24-25 ('a genuine large move is followed in steps, each later step being fresh and so bounded normally') claims a property the code does not have: the intermediate steps require honest intermediate medians that a pinned recipe over a gapped pool cannot produce. The NHI feed shares the cap: a control-plane outage that drops the index from 0.9 below 0.54 (services down and participation halved) leaves NHI stuck until health recovers, and the vault halted once it is 24h old. Reachable with the constants as committed. Smallest fix: let the bound grow with staleness (e.g. multiply the cap by the number of whole lifetimes elapsed, or lift it after N lifetimes) so a stale-feed re-anchor costs an attacker N hours during which anyone can refresh honestly for 0.5 IMD, while a genuine gap is followed within N hours; or accept two attestations with advancing windows that agree within the cap. Either preserves the internal audit's intent (no single-attestation re-anchor) without making a gap terminal.","line":327,"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 concrete SwarmFeed with the PriceFeed's launch policy (1-hour life, 2,000 bps cap) and an\n/// attester whose key this test holds. No question prefix, so only the deviation bound is exercised:\n/// the window rules are not what this finding is about.\ncontract GapFeed is SwarmFeed {\n    constructor(address attester_, address relayer_) SwarmFeed(attester_, relayer_, 1, 3, 1 hours, 2_000) {}\n}\n\n/// @notice FINDING: a genuine single-step move larger than STALE_DEVIATION_MULTIPLE x the cap (40%)\n/// can never be followed. `_checkValue` bounds every attestation against the last accepted value,\n/// 20% while fresh and 40% once stale, and the bound never widens further however long the feed has\n/// been stale. The pinned PriceFeed/SpotFeed questions read IMD's pool (a 13-sample median / the last\n/// block), so after the pool gaps 45% (IMD moved 44.6% in one day per the runbook) every honest answer\n/// is 0.55x the stale value and is refused with ExcessDeviation, forever, unless the market itself\n/// returns to within 40% of the old value. The feed is immutable and the vault's price-dependent\n/// actions (draw, free with debt, bark, bite, cash) require it fresh, so the protocol halts permanently.\n///\n/// The NatSpec says \"a genuine large move is followed in steps, each later step being fresh and so\n/// bounded normally\" -- but the steps do not exist: a step function sampled at any 13 blocks has a\n/// median equal to one of its two levels, and the spot feed reads one block.\n///\n/// This test fails on the committed code (the honest figure is refused at every attempt for a week)\n/// and passes once the bound can widen with staleness, lift after a bounded number of lifetimes, or\n/// accept two agreeing honest attestations.\ncontract FeedCannotFollowGapMoveTest is Test {\n    uint256 private constant ATTESTER_KEY = 0xA11CE;\n    uint256 private constant V = 1_000_000 gwei; // ~0.001 ETH per IMD, the live order of magnitude\n    GapFeed private feed;\n    uint256 private nonce;\n\n    function setUp() public {\n        vm.chainId(1);\n        vm.warp(10 days);\n        // This test is the relayer (SwarmRelay on mainnet, which anyone may call).\n        feed = new GapFeed(vm.addr(ATTESTER_KEY), address(this));\n        SwarmFeed.OracleAttestation memory a = _attestation(V);\n        feed.submitAttestation(a, _sign(a));\n        (uint256 value,) = feed.latestValue();\n        assertEq(value, V);\n    }\n\n    function test_aFeedCanEventuallyFollowAGenuine45PercentGap() public {\n        uint256 honest = V * 55 / 100; // the pool after a 45% fall; every honest answer is this\n        bool followed;\n        // One attempt an hour for a week: a single honest attestation, then a second honest one ten\n        // blocks later (so a fix that wants two agreeing readings also passes).\n        for (uint256 hour = 1; hour <= 7 * 24 && !followed; ++hour) {\n            vm.warp(10 days + hour * 1 hours + 1);\n            vm.roll(block.number + 300);\n            followed = _trySubmit(honest);\n            if (!followed) {\n                vm.roll(block.number + 10);\n                vm.warp(block.timestamp + 120);\n                followed = _trySubmit(honest);\n            }\n        }\n        (uint256 value,) = feed.latestValue();\n        assertTrue(followed, \"no honest attestation was ever accepted: the feed is bricked\");\n        assertEq(value, honest, \"the feed should read the market\");\n    }\n\n    /// @dev Companion: the same gap upward (a +50% rally while nobody bought a rise) is just as final.\n    function test_aFeedCanEventuallyFollowAGenuine50PercentRally() public {\n        uint256 honest = V * 150 / 100;\n        bool followed;\n        for (uint256 hour = 1; hour <= 7 * 24 && !followed; ++hour) {\n            vm.warp(10 days + hour * 1 hours + 1);\n            vm.roll(block.number + 300);\n            followed = _trySubmit(honest);\n        }\n        assertTrue(followed, \"no honest attestation was ever accepted: the feed is bricked\");\n    }\n\n    function _trySubmit(uint256 figure) private returns (bool ok) {\n        SwarmFeed.OracleAttestation memory a = _attestation(figure);\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) 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 ? block.number - 300 : 0);\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 = 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":"Feed at value V with updatedAt more than 1 hour old (its normal state). The pool gaps to 0.55V. submitAttestation with figure 0.55V (honest, correctly signed, advancing window within 300 blocks) -> expected: accepted at some point (the feed should be able to read the market); actual: ExcessDeviation on every attempt, every hour, for a week (test/scratch/FeedCannotFollowGapMove.t.sol, both tests fail: 45% fall and 50% rally). Meanwhile ParameterizedVault._pricingStale() is true and draw/bark/bite/cash all revert StaleFeed.","severity":"high","snippet":"            uint256 bound = _tooOld(_updatedAt) ? maxDeviationBps * STALE_DEVIATION_MULTIPLE : maxDeviationBps;","title":"SwarmFeed: the stale bound never widens, so a single-step market move above 40% (or 20% while fresh) can never be followed and the vault halts permanently"},{"citation":"resolved","description":"Q1. The multiplier arithmetic is right: with STALE_DEVIATION_MULTIPLE = 2 and a 2,000 bps cap, N attestations bought within one hour and relayed together move the value by at most 1.4 x 1.2^(N-1) (the first re-anchors a stale value at 40%, every later one is measured against a now-fresh value at 20%); six give 3.484. Nothing in SwarmFeed bounds N below the number of distinct advancing toBlocks inside the 300-block recency window, so N = 6 is an economic choice, not a contract limit. The window rules do not prevent the chain: with span 300 and a monotone pool path, window [T-300, T]'s 13-sample median is the sample at T-150, so six windows ending at B+150..B+155 have medians equal to the pool price at blocks B..B+5. Two errors in the cost arithmetic follow. (1) Using the doc's own per-rung fees ($58k, $93k, $130k, $174k, $221k, $273k, cumulative $281k/$455k/$676k/$949k at rungs 3-6) and gain = line x (1 - 1.7/m), the walk breaks even at line = $1.79M (2.016x), $1.53M (2.419x), $1.63M (2.903x), $1.85M (3.484x). The cheapest profitable walk flips at a $1.53M line, so the revisit rule's threshold ('roughly HALF the pool's depth (about $2M today)') is above the point where the attack is already positive: a proposeLine of $1.6M-$2M would pass the stated rule while the 4-rung walk nets ~$20k-$140k. (2) The $950k assumes the pool is pushed and restored at each of seven sample blocks per rung. An attacker who instead ramps the pool 20% per block at blocks B..B+5 and HOLDS it at 3.48x for the following 150 blocks produces the same six medians (samples before B sit at the base, the centre sample at the rung, samples after B+5 at the top) and the six spot readings at B..B+5, and pays one round trip: 841 x (sqrt(3.484) - 1) = 729 ETH (~$1.97M) in and out at 1% = ~$39k (rung 4 alone: ~$25k). At the committed $1M line the gain is $297k-$512k, so 'round-trip fees alone ... negative at every rung' does not hold across strategies; what actually defends the walk at launch is the attacker's exposure to IMD holders selling into a 3.5x pump during a ~30-minute hold, which the doc does not model. This is the one safety condition the doc attaches to raising line, so it should be restated with the hold model, a quantified exposure estimate, and a threshold at or below $1.5M (and lower if the pool thins). Doc inconsistency in the same file: line 36 still says the Treasury pays at 'half a feed's deviation bound'; the committed constants are a quarter of the cap on falls and never on rises.","line":184,"path":"docs/PARAMETERS-2026-10-05.md","reproduction":"Inputs as the doc states them: 841 ETH + 207,881 IMD, 1% fee, IMD $10.92 (ETH $2,699), mat 170, line $1M, cap 2,000 bps, STALE_DEVIATION_MULTIPLE 2. Expected per the doc: the walk is negative at every rung at today's line and flips only above ~$2M. Actual: cumulative doc fees / (1 - 1.7/m) gives $1.53M at m = 2.419 (four attestations), and one held ramp to 3.484x costs ~$39k in pool fees against a $512k gain at the committed $1M line. python3: X=841; eth=207881*10.92/X; fees=[58,93,130,174,221,273]; m=1.4; cum=0; for f in fees: cum+=f; print(m, cum/(1-1.7/m) if m>1.7 else None, X*(m**0.5-1)*eth*0.02); m*=1.2","severity":"medium","snippet":"$5M line makes the 3.48x walk worth ~$2.5M against ~$950k of fees. Any `proposeLine` above roughly HALF the","title":"PARAMETERS 'No rolling bound': the walk is profitable from a $1.53M line under the doc's own fee model (not ~$2M), and the fee model counts seven round trips per rung where one held ramp costs ~$39k f"},{"citation":"resolved","description":"Q5/Q9 (the Treasury-vault-asker seam at day zero). Treasury.fundOracle pays the asker only from sIMD the Treasury already holds (`available = maxWithdraw(treasury)`, Treasury.sol:494), and the Treasury's only sIMD income is the protocol's liquidation cut and cover's dust sweeps; stability fees arrive as imdUSD, which the Intake does not take. On a freshly deployed stack the Treasury holds nothing, fundOracle returns 0 without reverting, OracleAsker.ask(nhiFeed, body) reverts inside IIntake.request when the Intake pulls 0.5 IMD from an asker with no balance, and NhiFeed -- seeded by hand in runbook step 7.1 -- goes stale 24 hours later. _pricingStale() then refuses draw, free-with-debt, bark, bite, cash and cover's dust path for the whole vault until an NHI attestation is bought with askPaid. The keeper policy in docs/PARAMETERS-2026-10-05.md:190-192 buys with its own IMD only for a fall past the trigger and 'never to refresh a quiet stale price', so nothing in the documented sequence keeps NHI alive until the first liquidation happens to land sIMD in the Treasury (and 15 IMD/day of budget needs ~2 sIMD a day thereafter). Neither verify() nor the runbook's section 7 checks or seeds the oracle budget. Smallest fix: a runbook step before announcing that transfers IMD straight to ORACLE_ASKER (fundOracle tops up only to the budget, so prefunding the asker is harmless) or sIMD to the Treasury sized for N days of NHI asks, plus a line in verify()/the keeper's start-up that refuses to run while asker IMD + Treasury maxWithdraw < one day's budget; or have the keeper's fallback cover a near-stale keep-alive feed as well as a fall.","line":328,"path":"docs/MAINNET-RUNBOOK.md","reproduction":"State: stack deployed per DeployMainnet; three feeds seeded by hand (runbook 7.1); no deposits yet or only healthy ones. t = seed + 18h: OracleAsker.nearStale(nhiFeed) is true; Treasury.fundOracle() returns 0 (Treasury sIMD balance 0, maxWithdraw 0); OracleAsker.ask(nhiFeed, nhiBody) -> expected: a request is bought; actual: reverts in the Intake's transferFrom (asker IMD balance 0; with the rehearsal's MockIntake: ERC20InsufficientBalance). t = seed + 24h + 1s: NhiFeed.isStale() == true; ParameterizedVault.draw(1) reverts StaleFeed, bark/bite/cash likewise. The fork rehearsal does not reach this state because it never advances 24 hours and seeds NHI by storage write each time it warps.","severity":"medium","snippet":"4. **Start the keeper** before announcing. An unattended protocol with live positions and no","title":"Launch sequence: the Treasury holds no sIMD until the first liquidation, so the NHI keep-alive is unfunded and the vault halts 24 hours after the hand-seeded NHI value unless someone buys NHI updates "},{"citation":"resolved","description":"Q4 (the lock(1) freeze, F2). The dust path sweeps collateral strictly below _oneWeiSeizure(price) = 1.2e18 / price raw units, which at the launch sIMD price (~8.7e13 per 1e18 raw) is ~13,800 raw units, i.e. 1.4e-20 sIMD. Collateral of that seizure plus one unit is worth nothing but is 'collateral a bite could still reach', so cover reverts NoRealizedBadDebt and the only way back to a coverable position is bark, the full lull (6 h at NHI >= 0.85) and a bite that pays the liquidator ~14k raw units. Unlike accepted finding D9 (re-collateralising to health costs a real, fee-paying position), this costs the drained borrower one lock transaction per cycle and no capital, so the Treasury imdUSD held behind BadDebtFirst (Treasury.withdraw, payStream) for that position's record can be kept there indefinitely by a borrower who re-locks after each bite; totalBadDebt also keeps growing with fees meanwhile (internal audit info 8). Griefing only: no funds move to the attacker. Not reachable while a mark is live (a one-wei bite sweeps it at once), so the cadence is one lock per grace+tail (~7 h). Smallest fix: let cover sweep any collateral whose value at `price` is below some floor that is dust in economic terms (e.g. below 1e-6 of the position's debt, or below the seizure for `debt / 1e6` wei), or allow cover on a marked position once its grace has elapsed.","line":528,"path":"src/CDPVault.sol","reproduction":"test/scratch/CoverDustAboveSeizure.t.sol (passes: it demonstrates the state). Borrower locks 20e24 raw, draws 1,000 imdUSD; price halves; bark; +6h; bite drains the position (bad debt 276.7); cover(borrower, 1e18) succeeds. Mark expires (+1h+1s); borrower lock(27,650) raw (one above _oneWeiSeizure = 27,649 at the halved price). Expected: cover(borrower, 1e18) burns Treasury imdUSD against the realized bad debt. Actual: NoRealizedBadDebt; bite(borrower, 1) reverts MarkExpired; after bark it reverts GracePeriodNotElapsed for 6 hours; only then does a 1-wei bite sweep the dust and cover work again, and the borrower can lock again.","severity":"low","snippet":"            if (position.collateral >= _oneWeiSeizure(price)) revert NoRealizedBadDebt();","title":"cover: a drained borrower re-locking ~2.8e-20 sIMD (one raw unit above the one-wei seizure) blocks cover for a full mark+grace cycle, repeatably and for free"},{"citation":"resolved","description":"Q7/comment audit. The comment on FEED_MAX_DEVIATION_BPS still states the symmetric half-cap trigger. The committed constants are DRIFT_FALL_TRIGGER_OF_CAP_BPS = 2,500 (a 5% fall) and DRIFT_RISE_TRIGGER_OF_CAP_BPS = 0 (never for a rise), and OracleAsker.triggerBps returns (500, 0) for a 2,000 bps feed. Same stale statement at docs/PARAMETERS-2026-10-05.md:36 ('drifted more than half a feed's deviation bound'), which the Final confirmation section of the same file contradicts. Also unmentioned: the refusal past 20% is only while the value is fresh; past 40% once stale. Fix: restate as a quarter of the cap on a fall, never on a rise, 20%/40% fresh/stale.","line":95,"path":"script/DeployMainnet.s.sol","reproduction":"Deploy per the script and call OracleAsker.triggerBps(priceFeed): expected per the comment (1000, 1000); actual (500, 0). With the pool 8% BELOW the feed, arm() succeeds; with the pool 8% (or 19%) ABOVE the feed, arm() reverts NotNeeded, contrary to the comment's 'Treasury-paid update at 10% drift'.","severity":"low","snippet":"    /// HALF of this, so 2000 means a Treasury-paid update at 10% drift and a refusal past 20%.","title":"DeployMainnet NatSpec: 'OracleAsker asks on pool drift at HALF of this ... 10% drift' describes the trigger 84182f0 replaced (a quarter of the cap on falls, never on rises)"},{"citation":"resolved","description":"Q5/Q7. verify() reads back every wiring link between contracts this script deploys, but not the two reads that decide whether the Treasury ever pays for an update: (1) OracleAsker.price() = IIntake(INTAKE).priceOf(ORACLE_ACTION, IMD). If the swarm's Intake does not list IMD for 'oracle.request@oracle-1', or lists it above ASK_MAX_PRICE (1 IMD), every ask() reverts NotSold / PriceTooHigh, the NHI keep-alive never fires, NhiFeed goes stale 24 h after its first seed and the vault halts; the preflight checks only INTAKE.code.length. ASK_MAX_PRICE is a constant, so the only recovery is askPaid by hand at whatever price. (2) OracleAsker.poolPrice() != 0: IMD_POOL_ID and the extsload slot (keccak256(poolId, 6)) are pinned constants; a wrong id reads as 'no drift' forever and the Treasury never pays for a fall (I read the slot live on 2026-10-07: sqrtPriceX96 = 1330305773205498520043022607746, i.e. IMD ~0.00355 ETH, so the committed constants are right today, but nothing in the script proves it at deploy time). Other values not read back: Parameters.gap (50), parameters.workOracle() == 0, usdPriceFeed.maxAge() == 1 hour, triggerBps(feed) == (500, 0). Smallest fix: in verify(), require asker.price() != 0 && asker.price() <= ASK_MAX_PRICE, require asker.poolPrice() != 0, require vault.gap() == 50 and parameters.workOracle() == address(0).","line":307,"path":"script/DeployMainnet.s.sol","reproduction":"Run `forge script script/DeployMainnet.s.sol --sig 'verify((address,address,address,address,address,address,address,address,bytes,bytes,bytes))'` (or run()) against a fork where the Intake at INTAKE has code but priceOf(ORACLE_ACTION, IMD) returns 0 (the rehearsal's MockIntake before setPrice does exactly this): expected: verification fails naming the dead Treasury path; actual: 'Deployed and verified' is printed, and the first OracleAsker.ask(nhiFeed, body) on the live stack reverts NotSold.","severity":"low","snippet":"            require(tracksPool == tracks[i] && keepAlive == alive[i], \"asker: wrong trigger policy\");","title":"DeployMainnet.verify never reads the two external links the Treasury-paid oracle path depends on: the Intake's IMD price for oracle.request and the pool slot behind poolPrice()"}],"hash":"077db6979ab4dbfb2cef70c3b0d5101426e2bc80b1be651087f7e7fe2010253c","nodeId":"54bce762-b32d-4e29-ae34-8ab5c1bedb43","outcome":"completed","summary":"partial review: the turn budget ran out with 6 finding(s) written.\n","treeHash":null,"usage":{"cachedInputTokens":12619358,"inputTokens":112,"model":"claude-fable-5-1","outputTokens":111803,"runtime":"claude","turns":57,"wallClockMs":1826025}}],"verification":[]}