# Audit report

> Audit the whole protocol: every contract in src/, script/DeployMainnet.s.sol, script/DeployPreflight.sol, deploy/mainnet/ and docs/MAINNET-RUNBOOK.md section 7, at the pinned commit, for a mainnet launch. Seventeen audit rounds and their fixes are in (docs/AUDIT-*.md; the chain is web/content/docs/reference/audit-history.md). This is the LAST review before mainnet, of the WHOLE PROTOCOL at the commit that will deploy: every contract in src/, the deployment and the launch runbook, each mechanism in turn. Since the previous sweep (e4baedf, docs/AUDIT-FINAL-SWEEP-3-2026-10-09.md): the paced debt's netting reverted to the transaction's own mint, so the figure errs low, never high; the Treasury's paying functions gained a transient reentrancy guard; comments restated: git diff e4baedf c7d50ee -- src script. ACCEPTED items, each with its bound stated where it lives, are findings only if the stated bound is wrong or the reason does not hold: liquidation at a held-down pool (CDPVault.bite: 1.2/(1-push) of the debt at the real price, from the borrower; a thin position's remainder as bad debt), the payout price's gain per hour of hold and its lag after a rise or an honest fall (PAYOUT_PRICE_FALL_BPS_PER_HOUR), the dip and stale-term read (the paced figures), the paced debt erring low (_tallyPrincipalRetired), the fee-base floor, the work ceiling as an aggregate once the wage is on, the oracle's walk cost (docs/PARAMETERS-2026-10-05.md). Rank severity by what a finding lets someone take or block with the constants as committed (LINE $1M, wage 0, mat 170 at NHI >= 0.85, grace 6 hours, CHOP 20%, the backing's rise 2 points of par an hour, the follow 10% an hour, the payout price's fall 1% an hour, fee floor 100,000, fee cap 5%, FEED_MAX_DEVIATION_BPS 2000, SKEW_BPS 500, TIMELOCK 48 hours).
>
> imdUSD 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. The oracle's feeds read one full-range Uniswap v4 pool on Ethereum, about $2.3M a side with a 1% fee; IMD also trades in other pools and on Base and Robinhood Chain, so a price held off-market in the oracle's pool is open to arbitrage from those venues. docs/PARAMETERS-2026-10-05.md has the numbers every economic parameter was chosen from.
>
> Answer each numbered question, including the ones where nothing is wrong:
> 1. THE ORACLE (SwarmFeed, PriceFeed, SpotFeed, NhiFeed, UsdPriceFeed, SharePriceFeed, SwarmRelay, OracleAsker): attestation checks (signer, domain, question binding, window, replay), the epoch and deviation rules after silence, the first value, Chainlink staleness and failure, the asker's triggers, budget, back-off and callback, relay bundles. Anything that lands a value the question does not support, holds a feed off, or spends the Treasury's budget for nothing.
> 2. THE VAULT'S BORROWING AND LIQUIDATION (lock, lockIMD, free, draw, wipe, bark, barkFor, heel, bite, cover, the stability fee, dust, bad debt, the debt ceiling): every ordering by one or several positions and the relay; anything that leaves debt unbacked, frees collateral a position needs, stops a liquidation that should happen, or takes more than the stated bounds.
> 3. REDEMPTION AND THE PACED FIGURES (cash, the paced backing, supply, debt and payout price, resecure, the fee base and ratchet, the reserve route): anything that pays a redeemer more than the honest backing at the paid price, or blocks honest redemptions beyond the stated lags.
> 4. THE TREASURY (sync, withdraw, payStream, fundOracle, redeemIMD, the reserve register and its valuation, launch fees, the new guard): every exit bounded as documented, bad debt first, nothing an outsider can take, freeze or mis-record.
> 5. GOVERNANCE AND MINTING FROM WORK (Parameters, the timelock and every bound, Governed, SwarmWorkOracle, WorkOracleFactory, earn, earnLine, backedDebt): anything a governor can do beyond the bounds or faster than 48 hours, and anything that mints work against backing that is not there if the wage is turned on.
> 6. IMDUSD, THE FACTORIES AND THE DEPLOYMENT (ImdUSD, TreasuryFactory, DeployMainnet.run, verifySeeded, runVault with VAULT_SALT, verify, plan.py, the pinned bodies) and the launch window hour by hour against the runbook: what can be deployed wrong and pass, what a stranger can do between the stages, every way the protocol can halt on day one and how each recovers.
> 7. REENTRANCY AND EXTERNAL CALLS across every contract: each external call, what it can call back into, and whether state is written before it.
> 8. Every comment, NatSpec or runbook line that claims a property the code does not have, and the list of what you read in full and what you could not reach.
>
> Not 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.
>
> For 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.

| | |
|---|---|
| Repository | https://github.com/fa11up/infer-protocol |
| Commit | `c7d50ee0376885bc3413cffa16415425ed95c13e` |
| Job | `6229d0fc-c6a5-4c3e-bb56-64461d50b1cb` |
| Judged | 2026-10-09 15:59 UTC |
| Findings | 1 medium · 2 low · 4 info |

Four agents audited the code as it is at `c7d50ee`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: SwarmFeed: a value relayed at the end of a live epoch anchors the next one, so one end-of-block spot push attested honestly refuses the honest spot for two hours and halts every price action in the va

`src/SwarmFeed.sol:416`

```
        return (_value, _allowanceNow());
```

Oracle (question 1, 'holds a feed off'; question 2, 'stops a liquidation that should happen'). `_epoch()` anchors every new epoch at `_value`, whatever was accepted last, with the fresh allowance (maxDeviationBps, 20%) unless that value has been stale a whole STALE_GROWTH_PERIOD past maxAge (`_allowanceNow`, lines 429-444; `_accept`, lines 477-479). The epoch bound stops a WALK, but nothing lets an honest value UNDO a move made just before an epoch expired: a value accepted at t0+59min inside an epoch anchored at V (so up to 20% off V) becomes the anchor of the epoch that opens on the next acceptance after t0+60min, and the honest V, 25% above a 0.8V value, is refused `ExcessDeviation` until t0+59min+2h (the stale base is earned only by a whole hour of silence past the lifetime). For the spot feed this costs ONE block of the pool: the spot recipe reads the window's last block alone (SpotFeed.sol lines 46-48, samples 1), the buyer chooses toBlock (any block within maxAge/12 of head, span 150..1200), and the panel attests a pushed end-of-block state honestly. Sequence from an external caller: (1) read `epoch()` to learn when the live spot epoch opened (t0); (2) at about t0+50min sell about 24k IMD into the v4 pool as the last transaction of a block (a ~20% fall) and buy it back at the top of the next block (about $5k round trip in pool fees at launch depth, plus whatever arbitrage lands between the two bundles); (3) buy a spot attestation with toBlock at the pushed block and relay it through SwarmRelay at t0+59min (within the live epoch's 20% of V, so accepted); (4) from t0+60min every honest spot reading is refused for two hours. With the primary at V and spot at 0.8V, CDPVault._requirePriceAgreement reverts `PriceDivergence` (SKEW_BPS 500) for the first hour, then `StaleFeed` once the pushed spot ages past SPOT_MAX_AGE, so `draw`, priced `free`, `cash`, `barkFor`, `heel`, `bite`, `resecure` and a finite-ceiling `earn` all stop. A mark whose one-hour bite window (`tail()`) falls inside the halt expires (`_expired`) and must be retaken with a fresh six-hour grace, so a marked borrower can hold off their own liquidation for the cost of the push per seven hours; redemptions, the peg's defence, are closed for two hours. The Treasury does not pay to undo it (the spot feed is not keep-alive in DeployMainnet's asker policy, and the restored pool reads as a RISE against the 0.8V value, which DRIFT_RISE_TRIGGER_OF_CAP_BPS = 0 never buys), so the halt is not self-correcting; a borrower's askPaid for the spot during the lockout buys an honest answer the feed refuses, for the caller's 0.5 IMD. Repeatable: when the honest V lands at t0+59min+2h it opens a wide epoch anchored at 0.8V with `_epochFirst` = V, and the same push at that epoch's end re-anchors the next at 0.8V again. Reachable with the constants as committed (maxAge 1h, cap 2000, SKEW_BPS 500, grace 6h, tail 1h). The same premise is stated as a property in three places that the code does not have: SwarmFeed.sol lines 38-39 ('a far re-anchor cost an attacker those same hours of silence, during which anyone can refresh the feed') and 425-428, DeploymentConfig.sol lines 243-250 (WIDE_ALLOWANCE_BPS), and the accepted bite bound at CDPVault.sol lines 1349-1352 ('the cost of the hold ... for seven hours every arbitrageur ... must be absorbed'): a value pushed in through the end of a live epoch needs no silence and no hold, and nobody can refresh over it for two hours. Not previously recorded: docs/AUDIT-SWEEP-PANEL-ORACLE-2026-10-07.md item 6 covers two steps straddling a boundary in the WALK direction only; no earlier round states the lockout of the honest value, and PARAMETERS-2026-10-05.md lines 199-200 repeat the silence premise. Smallest fix: in `_checkValue`/`_accept`, when the stored epoch has expired, also accept a value within maxDeviationBps of the EXPIRED epoch's anchor (`_anchorValue`, or `_value` for a first epoch) and anchor the new epoch at that anchor in that case, so a

**Reproduction**

test/scratch/Proof_73542bc9e37c.t.sol (run: forge test --match-path test/scratch/Proof_73542bc9e37c.t.sol). Plain CDPVault over MockIMD with the shipped PriceFeed, NhiFeed and SpotFeed artifacts (maxAge 1h/1d/1h, cap 2000) seeded through `_accept` (the signature path is the attester's; `_checkValue`/`_accept` are exactly what submitAttestation runs). t0: nhi 0.9e18, primary V=4e15, spot V; borrower locks 1000 IMD and draws 1 imdUSD (works). t0+59min: spot.seed(0.8V) is ACCEPTED (inside the live epoch anchored at V, allowance 20%). t0+61min: primary.seed(V) lands; spot.seed(V) EXPECTED to land (0% from the previous anchor V), ACTUAL reverts `ExcessDeviation` (the new epoch anchored at 0.8V with allowance 20%; V is +25%). Verified by running it: '[FAIL: ExcessDeviation()] test_honestSpotLandsOnceTheLatePushsEpochHasExpired'. With spot stuck at 0.8V and the primary at V, vault.draw(1e18) reverts PriceDivergence (SKEW 500) until the pushed spot is stale, then StaleFeed; by `_allowanceNow` the honest V is accepted only at t0+59min+2h, when the 0.8V value has been stale a whole hour.

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

// A value relayed in the last minute of a live SwarmFeed epoch becomes the next epoch's anchor. For the spot
// feed, whose recipe reads one block, that is one end-of-block push of the pool (restored the next block),
// attested honestly: the honest spot, 25% above a 0.8x push, is then refused ExcessDeviation until the pushed
// value has been stale a whole hour (two hours after the push), and the vault refuses every price action
// (PriceDivergence for the first hour, StaleFeed for the second). This test FAILS on the committed code at
// the honest re-seed and passes once a value within maxDeviationBps of the expired epoch's anchor is accepted
// when a new epoch opens (and anchors the new epoch there).

import {Test} from "forge-std/Test.sol";
import {CDPVault} from "src/CDPVault.sol";
import {SwarmFeed} from "src/SwarmFeed.sol";
import {PriceFeed} from "src/PriceFeed.sol";
import {NhiFeed} from "src/NhiFeed.sol";
import {SpotFeed} from "src/SpotFeed.sol";
import {MockIMD} from "src/MockIMD.sol";
import {APPROVED_OPERATOR} from "src/DeploymentConfig.sol";

contract LockoutProofPriceFeed is PriceFeed {
    constructor() PriceFeed(1 hours, 2000) {}

    function seed(uint256 v) external {
        _accept(v, uint64(block.timestamp));
    }
}

contract LockoutProofNhiFeed is NhiFeed {
    constructor() NhiFeed(1 days, 2000) {}

    function seed(uint256 v) external {
        _accept(v, uint64(block.timestamp));
    }
}

contract LockoutProofSpotFeed is SpotFeed {
    constructor() SpotFeed(1 hours, 2000) {}

    function seed(uint256 v) external {
        _accept(v, uint64(block.timestamp));
    }
}

contract SpotEpochLockoutProofTest is Test {
    address private constant BORROWER = address(0xB0B);
    uint256 private constant V = 4e15; // wei of ETH per 1e18 IMD: the honest spot and primary

    MockIMD private imd;
    CDPVault private vault;
    LockoutProofPriceFeed private primary;
    LockoutProofNhiFeed private health;
    LockoutProofSpotFeed private spot;

    function setUp() public {
        vm.warp(1_000_000);
        vm.roll(20_000_000);
        imd = new MockIMD();
        primary = new LockoutProofPriceFeed();
        health = new LockoutProofNhiFeed();
        spot = new LockoutProofSpotFeed();
        vault = new CDPVault(address(imd), address(0), address(0), address(primary), address(health), address(spot));
        vm.prank(APPROVED_OPERATOR);
        imd.mint(BORROWER, 1_000_000 ether);
        vm.prank(BORROWER);
        imd.approve(address(vault), type(uint256).max);
    }

    /// @dev t0: an honest refresh (anyone's 0.5 IMD) opens a live spot epoch anchored at V. t0 + 59 min: the
    /// attacker relays a spot attestation of 0.8 V, an honest reading of the one block the pool was pushed in
    /// (inside the epoch's 20% of V, so accepted). t0 + 61 min: the epoch has expired; the honest V is within
    /// 20% of the expired epoch's anchor and must land, so the vault's price actions resume. On the committed
    /// code the new epoch anchors at 0.8 V with the fresh 20% allowance, V is 25% above it, and this reverts
    /// ExcessDeviation; the vault then refuses draw, bark, bite and cash for two hours.
    function test_honestSpotLandsOnceTheLatePushsEpochHasExpired() public {
        health.seed(0.9e18);
        primary.seed(V);
        spot.seed(V);
        uint256 t0 = block.timestamp;
        vm.startPrank(BORROWER);
        vault.lock(1000 ether);
        vault.draw(1 ether);
        vm.stopPrank();

        vm.warp(t0 + 59 minutes);
        vm.roll(block.number + 295);
        spot.seed(V * 8 / 10); // inside the live epoch: anchor V, allowance 20%

        vm.warp(t0 + 61 minutes);
        vm.roll(block.number + 10);
        primary.seed(V);
        spot.seed(V); // committed code: ExcessDeviation, the next epoch anchored at 0.8 V
        (uint256 value,) = spot.latestValue();
        assertEq(value, V, "the honest spot lands once the push's epoch has expired");
        vm.prank(BORROWER);
        vault.draw(1 ether); // and the vault's price actions resume at agreeing prices
    }
}
```

### 2. Low: Parameters and DeploymentConfig NatSpec state earnLine's ratio term as a share of totalDebt; the code uses backedDebt (capped at the transaction's opening debt and the paced debt, less totalBadDebt)

`src/Parameters.sol:142`

```
    /// @notice The live ratio term of the vault's work ceiling, in basis points of totalDebt.
```

Question 8 (comment claims). Parameters.sol:142 and DeploymentConfig.sol:144 (`earnLine = reserveValueUsd + totalDebt * EARN_MAT_BPS / 10000`) describe the work ceiling's ratio term as a share of totalDebt. ParameterizedVault.earnLine (lines 279-281) computes `reserveValue() + mulDiv(backedDebt(), parameters.earnMat(), 10_000)`, and backedDebt (lines 262-270) is `min(totalDebt, debtAtTransactionStart, pacedDebtNow) - totalBadDebt`. The difference is the whole point of the D1 fix: with a fresh $1M book the paced debt is about 10,000 imdUSD after one paced hour (FOLLOW 10% an hour of the 100,000 floor), so earnLine is 2,500 imdUSD, not 250,000. A governor sizing a wage proposal from Parameters, or a reader checking the work ceiling against the stated formula, gets a figure up to two orders of magnitude too high in the day after any large draw. The code is right; both comments are wrong. Doc-only, no funds at risk; the wage is 0 at launch. Fix: change both comments to 'of backedDebt (totalDebt capped at the transaction's opening debt and the paced debt, less totalBadDebt)'.

**Reproduction**

Read ParameterizedVault.sol:279-281 and 262-270 against Parameters.sol:142 and DeploymentConfig.sol:144. State: ParameterizedVault with one position that drew 1,000,000 imdUSD one block ago, empty register, EARN_MAT_BPS 2500, one paced hour elapsed. Expected from the NatSpec: earnLine() = 0 + 1,000,000 * 0.25 = 250,000e18. Actual: backedDebt() = min(1e24, 1e24, _pacedDebtNow()) where the paced debt has followed at most 10% of max(live, 100,000e18 floor) per paced hour, about 10,000e18, so earnLine() = 2,500e18.

### 3. Low: bite comment 'a bite never seizes more than the formula' and the function NatSpec omit the remainder sweep twenty lines below, which adds dust above the formula's payout

`src/CDPVault.sol:1380`

```
            // Any larger shortfall is still refused: a bite never seizes more than the formula.
```

Question 8. CDPVault.bite line 1380 states the seizure never exceeds floor(debtToRepay * 1.2e18 / price). Lines 1403-1407 then fold in `remainder` (the collateral left after the seizure when it is below _oneWeiSeizure(price) and debt survives), so the transfer is formula + remainder. The function NatSpec at 1332-1334 ('Collateral must cover the full payout, except dust below the seizure for one wei of debt, which is taken whole') also omits the sweep. The sweep is deliberate and correct (it is what makes _recordBadDebt reachable), but an integrator computing the liquidator's receipt from line 1380, or the borrower's loss from the NatSpec, is off by the dust, and the sentence at 1380 is false as written. Doc-only. Fix: reword 1380 to 'a bite never seizes more than the formula plus a remainder too small for any later bite (below)' and add the same clause at 1332-1334.

**Reproduction**

State: position with debt 2e18 and collateral exactly floor(1e18 * 1.2e18 / price) + k raw units, 0 < k < _oneWeiSeizure(price); fresh agreeing feeds; the position marked and past grace. Call bite(owner, 1e18). Expected per line 1380: collateralSeized = floor(1e18 * 1.2e18 / price). Actual (lines 1403-1407): collateralSeized = that + k, emitted as Bite.collateralSeized, since remainder = k != 0, debtToRepay < debt and k < _oneWeiSeizure(price). test/scratch/BiteDustJudge.t.sol's first bite shows the same mechanism: the largest formula seizure leaves collateral 0 (the raw remainder swept) and the position drained.

### 4. Info: bite's dust branch accepts any debtToRepay, so a keeper repaying the full debt against sub-one-wei dust burns its whole repayment for one raw unit

`src/CDPVault.sol:1384`

```
            collateralSeized = position.collateral;
```

Question 2. When `collateralSeized > position.collateral` and the collateral is below `_oneWeiSeizure(price)`, bite seizes the whole remainder instead of reverting (lines 1375-1385). The branch exists so one wei of debt can clear dust no formula seizure reaches, but it does not bound `debtToRepay`: any amount up to the position's full accrued debt passes the ExcessRepayment check at 1373, is burned from the caller by `_payDebt`, and is paid with the dust. The bad debt is then retired by the liquidator rather than by `cover` (the Treasury) and totalBadDebt falls. Nobody else can take anything (the loss is the caller's own imdUSD, and the protocol gains), so this is a keeper footgun, not a theft: a keeper that computes debtToRepay from positions(owner).debt for a drained-then-relocked position burns its inventory for one raw unit. Reachable with the constants as committed. Smallest fix: in the dust branch require `debtToRepay == 1` (or at most the dust's value in debt plus one wei) before `collateralSeized = position.collateral;`, so the remainder of the debt stays on the `cover` path.

**Reproduction**

test/scratch/BiteDustJudge.t.sol test_dustBranchAcceptsFullDebtForOneRawUnit (PASSES on this code: it demonstrates the state). Base CDPVault over MockIMD, price feeds 1e18, NHI 0.9. Borrower locks 1,700 IMD, draws 1,000 imdUSD; keeper locks 5,000, draws 2,000. Price steps 1 -> 0.8 -> 0.64 -> 0.512 an hour apart; keeper barks; after lull()+ the keeper bites the largest coverable debt (1700e18*0.512e18/1.2e18): collateral 0, debt 274.713e18 recorded as totalBadDebt. Borrower calls lock(1). Keeper calls bite(borrower, 274713043378995433667), the full debt. Expected: refused, or bounded to the one wei the branch is written for, with the rest left to cover. Actual: succeeds; keeper burns 274.713 imdUSD and receives 1 raw unit; position debt 0; totalBadDebt 0 (logged by the test).

### 5. Info: heel NatSpec and the bite defence line say deposit and repayment clear a mark; they clear it only on a recovery observed at fresh, agreeing feeds

`src/CDPVault.sol:1320`

```
    /// when recovery is observed; deposit, repayment and successful borrowing/withdrawal also clear them.
```

Question 8. `lock`, `lockIMD` and `wipe` clear a mark through `_clearIfRecovered` (lines 1729-1740), which requires `priced != 0`, `_priceAgrees()` (fresh primary, NHI, collateral price and spot, spot within skew of the primary) and health at that price. While any feed is stale or the two price feeds diverge, a marked borrower who tops up or repays above mat keeps the mark; because grace has already elapsed it is actionable the moment feeds are fresh again if the price is then below recovery, with no new grace. `_clearIfRecovered`'s own NatSpec states the condition; the heel summary at 1319-1320 and the bite NatSpec's defence line at 1349 ('a marked borrower who tops up or repays above mat clears the mark') do not. Doc-only: state the fresh-and-agreeing condition in both places, or tell a marked borrower to call `heel` once feeds are fresh.

**Reproduction**

State: a position marked underwater; spot feed stale (SPOT_MAX_AGE one hour, bought on demand); primary recovers. Borrower calls lock(amount) bringing the ratio above mat. Expected per line 1320: the deposit clears the mark. Actual: `_clearIfRecovered` returns without clearing because `_priceAgrees()` is false (spot stale); `liquidationMarks[owner].marked` stays true, and once spot is refreshed at a price below recovery `bite` is open at once (grace already elapsed, within tail).

### 6. Info: UsdPriceFeed docstring says a dead ETH/USD leg only degrades the work ceiling; on ParameterizedVault it halts earn through _requireFreshFeeds

`src/UsdPriceFeed.sol:22`

```
/// it priced at nothing. Degrading the ceiling is the safe direction; bricking the channel is not.
```

Question 8. Lines 18-22 argue the raw staticcall keeps the work channel open: a dead or malformed aggregator reads as zero, reports stale, and 'values whatever it priced at nothing', so only the reserve term of earnLine degrades. That is true of the view earnLine(). It is not true of the channel: ParameterizedVault._pricingStale() (lines 226-228) is `super._pricingStale() || collateralPriceFeed.isStale()`, collateralPriceFeed is SharePriceFeed over this UsdPriceFeed, and CDPVault.earn calls _requireFreshFeeds() (line 576), so once the ETH/USD answer is older than ETH_USD_MAX_AGE, missing, non-positive or malformed, every earn reverts StaleFeed. The halt is the documented behaviour at ParameterizedVault.sol:220-225 ('a dead Chainlink ETH/USD leg stops minting, marking and liquidation here'); the UsdPriceFeed docstring predates the USD denomination. No funds at risk; halting is the safer direction. Fix: reword 18-22 to say the raw read keeps earnLine(), backingPerUnit() and reserveValueUsd() from REVERTING while the leg is down (views and the Treasury register stay readable, the reserve term reads zero), and that price-dependent actions including earn are refused by the vault's staleness check until Chainlink answers.

**Reproduction**

State: ParameterizedVault at the shipped constants, wage nonzero, a holder of minting rights. Input: CHAINLINK_ETH_USD.latestRoundData() returns updatedAt = block.timestamp - 2 hours - 1 (or answer <= 0, or fewer than 160 bytes). Call vault.earn(1). Expected from the docstring: the channel stays open with the reserve term of earnLine at zero. Actual: UsdPriceFeed.isStale() true (line 52), SharePriceFeed.isStale() true, ParameterizedVault._pricingStale() true, CDPVault._requireFreshFeeds() reverts StaleFeed() at line 576 before the ceiling is read. earnLine() itself does not revert and reports reserveValue() == 0, the half of the claim that holds.

### 7. Info: DeploymentConfig header comments describe the Sepolia release's roles (one-time links, mock faucets, a feed reporter, 'Chainlink ETH/USD on Sepolia') that the mainnet code does not have; OracleAsker q

`src/DeploymentConfig.sol:13`

```
/// MUST NOT be the feed's reporter or relayer — whoever sets the price would otherwise profit from
```

Question 8, merged from audit_permissions (lines 4 and 77) and audit_flow (line 13). Lines 4-7 say APPROVED_OPERATOR 'completes the two one-time links and operates the mock faucets': on the mainnet path (DeployMainnet._vaultInit passes stablecoin_ = 0 and WORK_ORACLE_SENTINEL) the vault creates a bound ImdUSD in its constructor (ImdUSD.setVault is permanently AlreadyInitialized) and a SwarmWorkOracle with no grantRights, so there are no links and no faucet; what the key actually holds is the Parameters governor (Governed.onlyGovernor: propose/cancel), Treasury.withdraw, withdrawNative and handOffLaunchFees, and ImdUSD's initializer only in the ImdUSD(address(0)) mode the deployment never uses. Line 13 says FEE_RECIPIENT 'MUST NOT be the feed's reporter or relayer': there is no reporter (SwarmFeed.report was removed, as lines 79-83 of the same file say; DeployMainnet.verifyFeeds asserts report(uint256) is unreachable) and the relayer is the permissionless SwarmRelay, so the sentence names roles nobody holds. Line 19 says 'Chainlink ETH/USD on Sepolia' and line 4 'the approved Sepolia workflow (miyagod.eth)': deploy/mainnet/plan.py --write rewrites the constants beneath them (and the runbook's section 3 table lists CHAINLINK_ETH_USD and ATTESTATION_RELAYER as must-change, so their current Sepolia values are not themselves a defect) but touches no comment, so the release commit will carry mainnet addresses under sentences that still say Sepolia. OracleAsker.sol:52 quotes a paid update at '~$4.25'; 0.5 IMD at the parameters doc's $10.92 is about $5.46. Nothing on chain depends on any of these; they mislead a reader of the deploy commit about who holds what. Fix: rewrite lines 4-7 to name the governor, withdrawer and launch-fee hand-off roles; delete or reword line 13; drop 'Sepolia' from lines 4 and 19 (or have plan.py rewrite them); update the OracleAsker figure.

**Reproduction**

grep -n APPROVED_OPERATOR src/*.sol: Governed.sol (propose/cancel), Treasury.sol (withdraw, withdrawNative, handOffLaunchFees), ImdUSD.sol (initializer only when constructed with address(0), which DeployMainnet never does). grep -n 'function report' src/SwarmFeed.sol: no match. script/DeployMainnet.s.sol _vaultInit: stablecoin_ = 0, oracle_ = WORK_ORACLE_SENTINEL. deploy/mainnet/plan.py lines 59-66: set_constant on APPROVED_OPERATOR, FEE_RECIPIENT, INTAKE and the planned addresses only; no comment is rewritten. Expected: the comments describe the shipped roles and the chain. Actual: they describe the Sepolia release's. `forge script script/DeployMainnet.s.sol --sig 'check()'` at c7d50ee prints CHANGE for ATTESTATION_RELAYER and CHAINLINK_ETH_USD (0x5f4eC3Df...) alongside the three placeholders, as the runbook's section 3 says it must until plan.py --write runs.

---

Judge's submission `197c469eab1ddb97b7fc9fd219d7b866ac00ae39ca99a6439330f3ee1a77302d`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
