# Audit report

> Audit the vault: src/CDPVault.sol, src/ParameterizedVault.sol, src/ImdUSD.sol and src/TransientReentrancyGuard.sol, in full, plus the deployment and the runbook's launch window, at the pinned commit, for a mainnet launch. Read whatever else in src/ these depend on, but report on this scope. Fifteen audit rounds and their fixes are in (docs/AUDIT-*.md; the chain is web/content/docs/reference/audit-history.md). The newest, docs/AUDIT-FINAL-SWEEP-2-2026-10-09.md, found one high: a pool held down through the feed's median window paid a redeemer the whole one-step fall in extra IMD. Its fix is this commit's newest mechanism: the price a redemption is PAID at is paced. The same commit clamps the paced debt against the pre-existing principal a transaction cancelled, seeds the paced supply no higher than the fee-base floor, reads the price and NHI once per entry point, and replaces the reentrancy guard with a transient-storage one: git diff a3aa9e4 c90e8d9 -- src. Read the vault in full, as it will deploy. A finding of an earlier round counts only if its fix regressed or left a gap. Items ACCEPTED, their reasons stated where they live, are findings only if the reason is wrong or the bound does not hold: the dip and the stale-term read (the paced figures' NatSpec), the paced payout price's lag after an honest fall (cash), the fee-base floor and its seed, the work ceiling as an aggregate once the wage is on. Rank severity by what a finding lets someone take or block with the constants as committed (LINE $1M, wage 0, mat 170, the rise 2 points of par an hour, the follow 10%, the payout price's fall 5%, PACE_INTERVAL 1 hour, fee floor 100,000, FEED_MAX_DEVIATION_BPS 2000).
>
> 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. 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.
>
> Answer each numbered question, including the ones where nothing is wrong:
> 1. THE PACED PAYOUT PRICE (cash, _pacedPrice, PAYOUT_PRICE_FALL_BPS_PER_HOUR, payoutPrice). cash pays IMD at max(attested price, paced price), the paced price falling at most PAYOUT_PRICE_FALL_BPS_PER_HOUR per hour of elapsed time (at most PACE_INTERVAL per pacing, on the backing's clock, written only at a fresh, agreed price) and rising at once; eligibility, health, RedemptionWorsensRatio and the collateral term stay at the attested price. With the feeds as committed (a one-step fall of FEED_MAX_DEVIATION_BPS from a fresh anchor, twice that after two silent hours, both feeds reading the one pool, the 13-sample two-hour median): the cheapest profitable push of the pool in either direction against redemptions, in money and hours, now; whether a RISE of the attested price (paid at once) or a sequence of falls and rises across pacings pays a redeemer more than the honest IMD; whether the mixed route's conversion of the reserve's IMD back into cancelled debt at the paid price can be made inconsistent with the candidate's share; and the cost to honest redeemers after a real fall, as a figure, against what cash's comment states.
> 2. THE CANCELLATION-AWARE CLAMP (_clampPacedDebt, CANCELLED_PRE_SLOT, PACED_DEBT_AT_START_SLOT, MINTED_BY_SLOT keyed per position by XOR, _tallyPrincipalRetired, _debtForPacing, the WIPED tally). Every ordering of draw, wipe, cash, bite and cover by one or several positions, in one transaction or across block boundaries: can zero-second debt count for the work ceiling sooner than the follow rate, can a transaction cancelling its own fresh draw move the paced debt, can the XOR-keyed slot collide with any fixed transient slot or another owner's, and can the netting of a position's own minted principal be used to cancel seasoned debt while reporting none?
> 3. THE PRICE READ ONCE. Every entry point reads the price and NHI once and passes them down (_requireFreshFeeds and _pace return the price; _paceAt, _healthy, _resecure, _reduceDebt, _clearIfRecovered take it). Is there any call in which a value used later was read before a check that should have gated it, any path in which the price passed differs from what the replaced read would have returned (the ungated calls' _priceOrZero, the gated calls' _price), and any external call inside an entry point (sIMD, the Treasury, the work oracle, the stablecoin) that could change a feed between the read and its use?
> 4. THE TRANSIENT GUARD (src/TransientReentrancyGuard.sol, on the vault and SwarmRelay): equivalent to OpenZeppelin's for every reentrant path the earlier rounds tested (the share vault, the work oracle, the Treasury, the relay bundles), including a reentrant call from a different guarded contract in the same transaction.
> 5. THE SEEDED PACED SUPPLY AND THE FEE: a paced supply of zero follows the live supply no higher than the floor; the paced figures otherwise as in record 24. The cheapest pin of the cap for everyone and the cheapest dilution, now, including across a book that empties and refills.
> 6. WHAT THE SWEEP'S JUDGE DID NOT REACH ITSELF (record 25, Coverage): resecure's griefing surface (a flood of re-prices, a re-price ordered before a liquidation or a redemption, a re-price at a divergent block); the launch window hour by hour against docs/MAINNET-RUNBOOK.md section 7 (the first values and verifySeeded, runVault with VAULT_SALT through a private relay, the keeper's duties: pace hourly, resecure after each update, bite with its own imdUSD); the deployment script (DeployMainnet.run, verifySeeded, runVault, _refuseAnotherVault, verify, plan.py) for what can be deployed wrong and pass; governance and the Treasury for regressions only.
> 7. LIQUIDATION, COVER, POSITIONS, ARITHMETIC AND SIZE for regressions after the refactor: bark/bite/heel/cover with the price and NHI passed in, the dust rules, bad debt, the stability fee, rounding in every division that pays someone, saturation; initcode 47,867 of 49,152 bytes.
> 8. Every comment or NatSpec in scope that claims a property the code does not have, cash's and the paced figures' first.
>
> 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 | `c90e8d9925855c32e21726e702a94446e994cfa6` |
| Job | `f936eafb-0bfb-4584-95be-9113b9d451ad` |
| Judged | 2026-10-09 06:44 UTC |
| Findings | 1 high · 3 low · 1 info |

Four agents audited the code as it is at `c90e8d9`, 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. High: cash: the paced payout price only delays the held-down-pool redemption; a pool held 20% down for five paced hours (or ramped 5% an hour) still pays a redeemer the whole fall, up to 18.75% of redeemed

`src/CDPVault.sol:1021`

```
        uint256 floor_ = paced
            - Math.mulDiv(paced, PAYOUT_PRICE_FALL_BPS_PER_HOUR * Math.min(elapsed, PACE_INTERVAL), 10_000 * 1 hours);
```

Merged from four specialists (audit_math 7d518055, audit_permissions 05be3eba, audit_economics b01da360, audit_flow 44b0f7ef); all four proofs fail on c90e8d9 for the stated reason.

The final-sweep-2 high (record 25, finding 1: a pool held down through the feed's median window paid a redeemer the whole one-step fall) was fixed by paying IMD at max(attested price, paced price), the paced price falling at most PAYOUT_PRICE_FALL_BPS_PER_HOUR (5%) per paced hour (_pacedPrice, written by _paceWith at every pacing at a usable price). _pacedPrice bounds the RATE of the fall only: floor_ = paced x (1 - 0.05 x min(elapsed, 1h)/1h) per pacing, with no lower bound except the attested price itself. pace() is permissionless and the runbook's keeper paces hourly, so an attacker who keeps the pool at the attested low simply waits: after n paced hours the paid price is max(0.8P, 0.95^n P), which reaches the attested low at n = 5 (0.95^5 = 0.774 < 0.80; four pacings leave 0.8145, so the record's 'four paced hours' is also wrong). The redemption then pays 50,000 x 0.95 / 0.80 = 59,375 IMD per 50,000 imdUSD at the 5% fee cap (which the attacker's own burns reach at 9% of the 100,000 fee base), i.e. 18.75% of redeemed volume taken from in-band candidates' collateral (at -20% every honest position under ~275% CR is in band) and from the Treasury's sIMD. Gain per imdUSD burned at hours 1..5: 0%, 5.3%, 10.8%, 16.6%, 18.75%; a 40% step after two silent hours is followed in ten paced hours for 58%. A ramp inside the feeds' allowance (5% an hour, each step within FEED_MAX_DEVIATION_BPS and spot within SKEW of the median) is followed with no lag at all and after five hours pays 61,387 IMD per 50,000 (the economics proof).

Cost with the constants as committed: ~12% of the pool's IMD side (~$240k) sold into the ~$2.3M-a-side 1%-fee pool (~$5k round trip), one attestation per hour (~$9 each; the Treasury's own 5%-fall drift trigger buys the first), and a hold of ~65 minutes (7 of 13 median samples) plus five paced hours instead of ~65 minutes. The hold's cost is the dip-buying absorbed in those hours, which record 25 left unquantified; IMD has no other market to arbitrage it back from. Takeable: up to 18.75% of in-band debt plus the reserve, about $187k at LINE $1M. This is exactly the attack the record rated high, delayed by about 4.4 hours; it is not the ACCEPTED 'paced payout price's lag after an honest fall' (that direction underpays redeemers and is correctly stated at cash).

Question 1's other parts, checked: a RISE of the attested price is paid at once and pays fewer IMD, and a sequence of falls and rises across pacings never pays more than the attested price would at the lowest point (paced = max(price, floor) at every pacing), so the only overpaying route is holding the attested price below the paced one. The mixed route is consistent: debtCancelled = amount - reserveOut x payPrice / payoutScale and the RedemptionWorsensRatio check both use payPrice, so the candidate's share equals its collateral x debtCancelled / debt bound exactly. The honest-fall cost is as cash states: after a real 20% fall redeemers receive 80%, 84%, 89%, 93%, 98% of the attested IMD at hours 0..4.

Comments claiming a property the code does not have, same mechanism: src/CDPVault.sol:349-353 ('holding IMD's pool down through the median window no longer pays a redeemer the fall in extra IMD'), docs/MAINNET-RUNBOOK.md:413-415 (same), docs/AUDIT-FINAL-SWEEP-2-2026-10-09.md Resolution #1 ('about break-even before the cost of the push', 'a 20% fall in four paced hours').

Smallest fix (a design decision between two bounded costs): either lower PAYOUT_PRICE_FALL_BPS_PER_HOUR so the hold needed exceeds what a pool can plausibly be held for (at 100 bps/h a 20% fall takes ~22 paced hours and honest redeemers are underpaid that long after a real fall), or keep the rate and add a slow reference the pool cannot move in one step (e.g. the paced price's 24-hour high, or a second paced p

**Reproduction**

test/scratch/Proof_05be3eba554b.t.sol (attached as proof; the three other specialists' proofs are the same fixture). ParameterizedVault over MockIMD at $1 (IMD/ETH 1/2000, Chainlink 2000e8), NHI 0.85 (mat 170, gap 50). BOOK locks 199,000 / draws 99,500 (200%, the candidate); HOLDER locks 300,000 / draws 100,000; 24 hourly pacings; backingPerUnit() == 1e18. Both feeds set to 0.80x; then five hourly re-attestations at 0.80x each followed by vault.pace(). payoutPrice() == 0.8e18. HOLDER cash(50_000e18, 0, BOOK). EXPECTED (the fix's claim): at most 50,000 IMD at the pre-fall price (about 52,500 at break-even plus the hour's 5%). ACTUAL on c90e8d9: gemOut = 59,375e18, all from BOOK's collateral, BOOK's debt down by exactly 50,000: 'a pool held down five hours pays the redeemer more than it burned: 59375000000000000000000 > 50000000000000000000000'. The second test holds two hours: 52,631.58e18 (already above break-even). The economics proof's 5%/hour ramp paced hourly: 61,386.88e18 after five hours. Hour count: after four hourly pacings at 0.8x payoutPrice() = 814370498958333336, after five 800000000000000000.

**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;

// The paced payout price (PAYOUT_PRICE_FALL_BPS_PER_HOUR = 500) follows a one-step 20% fall of the attested price
// in five paced hours (0.95^5 = 0.774 < 0.80). A pool held 20% down and kept attested hourly (the feeds' lifetime)
// therefore still pays a redeemer the whole fall in extra IMD after five hours; from the second hour the payout
// is already above what the imdUSD burned was worth. This test fails on c90e8d9: 50,000 imdUSD takes 59,375 IMD
// (worth $59,375 at the pre-fall price) out of the candidate's collateral after a five-hour hold.

import {Test} from "forge-std/Test.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {ParameterizedVault} from "src/ParameterizedVault.sol";
import {ImdUSD} from "src/ImdUSD.sol";
import {MockIMD} from "src/MockIMD.sol";
import {TreasuryFactory} from "src/TreasuryFactory.sol";
import {ISwarmFeed} from "src/interfaces/ISwarmFeed.sol";
import {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from "src/DeploymentConfig.sol";

contract HdFeed is ISwarmFeed {
    uint256 public constant maxAge = 1 hours;
    uint256 private value;
    uint64 private updatedAt;

    constructor(uint256 v) {
        set(v);
    }

    function set(uint256 v) public {
        value = v;
        updatedAt = uint64(block.timestamp);
    }

    function latestValue() external view returns (uint256, uint64) {
        return (value, updatedAt);
    }

    function isStale() external view returns (bool) {
        return block.timestamp - updatedAt > maxAge;
    }
}

contract HdAggregator {
    function decimals() external pure returns (uint8) {
        return 8;
    }

    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
        return (1, 2000e8, block.timestamp, block.timestamp, 1);
    }
}

contract HeldDownPoolRedemptionTest is Test {
    address private constant BOOK = address(0xB00C);
    address private constant HOLDER = address(0x401D);
    uint256 private constant DOLLAR = uint256(1 ether) * 1e18 / 2000 ether; // IMD/ETH at $1

    MockIMD private imd;
    ParameterizedVault private vault;
    ImdUSD private stable;
    HdFeed private primary;
    HdFeed private health;
    HdFeed private spot;
    uint256 private imdEth = DOLLAR;

    function setUp() public {
        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);
        vm.etch(CHAINLINK_ETH_USD, address(new HdAggregator()).code);
        vm.warp(1_000_000);
        imd = new MockIMD();
        primary = new HdFeed(DOLLAR);
        health = new HdFeed(0.85 ether); // mat 170, gap 50: a position at 200% is a candidate
        spot = new HdFeed(DOLLAR);
        vault = new ParameterizedVault(
            address(imd), address(0), address(0), address(primary), address(health), address(spot)
        );
        stable = vault.stablecoin();
        vm.startPrank(APPROVED_OPERATOR);
        imd.mint(BOOK, 200_000 ether);
        imd.mint(HOLDER, 300_000 ether);
        vm.stopPrank();
        vm.startPrank(BOOK);
        imd.approve(address(vault), type(uint256).max);
        vault.lock(199_000 ether);
        vault.draw(99_500 ether); // 200%: the candidate
        vm.stopPrank();
        vm.startPrank(HOLDER);
        imd.approve(address(vault), type(uint256).max);
        vault.lock(300_000 ether);
        vault.draw(100_000 ether); // the redeemer's imdUSD, held for a day
        vm.stopPrank();
        for (uint256 i; i < 24; ++i) _hour();
        assertEq(vault.backingPerUnit(), 1e18, "a par book");
    }

    function _next(uint256 seconds_) private {
        vm.warp(block.timestamp + seconds_);
        vm.roll(block.number + 1 + seconds_ / 12);
        primary.set(imdEth);
        spot.set(imdEth);
        health.set(0.85 ether);
    }

    function _hour() private {
        _next(1 hours);
        vault.pace();
    }

    /// @dev One step the feeds accept (20%), then the pool is held there and re-attested every hour for five
    /// hours (anyone may `pace`, and the Treasury's own fall trigger buys the first update). The next block a
    /// holder redeems against the candidate.
    function test_aPoolHeldDownFiveHoursStillPaysTheWholeFallInExtraIMD() public {
        uint256 preFall = DOLLAR;
        imdEth = DOLLAR * 80 / 100;
        _next(12);
        for (uint256 i; i < 5; ++i) _hour();
        (uint256 price,) = vault.collateralPriceFeed().latestValue();
        assertEq(price, 0.8 ether, "the vault prices at the attested low");
        assertEq(vault.payoutPrice(), 0.8 ether, "after five paced hours the paid price has followed the fall");
        (uint256 bookCollateralBefore, uint256 bookDebtBefore) = vault.positions(BOOK);
        vm.prank(HOLDER);
        uint256 gemOut = vault.cash(50_000 ether, 0, BOOK);
        (uint256 bookCollateralAfter, uint256 bookDebtAfter) = vault.positions(BOOK);
        assertEq(bookCollateralBefore - bookCollateralAfter, gemOut, "paid from the candidate");
        assertEq(bookDebtBefore - bookDebtAfter, 50_000 ether, "fifty thousand of debt cancelled");
        // EXPECTED: 50,000 imdUSD takes at most 50,000 IMD at the pre-fall price. ACTUAL on c90e8d9: 59,375 IMD.
        uint256 valueAtPreFall = Math.mulDiv(gemOut, preFall * 2000, 1e18);
        assertLe(valueAtPreFall, 50_000 ether, "a pool held down five hours pays the redeemer more than it burned");
    }

    /// @dev The same hold, two hours: already above break-even (the fee is at most 5%).
    function test_aPoolHeldDownTwoHoursAlreadyPaysMoreThanBurned() public {
        uint256 preFall = DOLLAR;
        imdEth = DOLLAR * 80 / 100;
        _next(12);
        for (uint256 i; i < 2; ++i) _hour();
        vm.prank(HOLDER);
        uint256 gemOut = vault.cash(50_000 ether, 0, BOOK);
        uint256 valueAtPreFall = Math.mulDiv(gemOut, preFall * 2000, 1e18);
        assertLe(valueAtPreFall, 50_000 ether, "a pool held down two hours pays the redeemer more than it burned");
    }
}
```

### 2. Low: cash: a pumped attested price is written into the paced payout price at once and decays at 5% a paced hour after the pool is released, so a one-window pump underpays every redemption by up to 17% for

`src/CDPVault.sol:1020`

```
        if (!_followRateLimited() || paced == 0 || price >= paced) return price;
```

Merged from audit_math 2356fdaa, audit_permissions 76d6681b, audit_economics e96d02c8, audit_flow 1c200b8b. _pacedPrice returns the attested price whenever it is at or above the stored paced price, and _paceWith stores that result, so a RISE is written into _pricePaced in full at the next pacing (anyone supplies one with pace() or lock(1)); when the attested price returns to honest, cash pays at max(attested, paced) = the pumped figure less 5% per paced hour. Before c90e8d9 a pump depressed payouts only while the attested price was pumped; now it depresses them for about four paced hours after the attested price is honest again. The push is the same size as the high's (both feeds read the one pool: ~$220-240k bought, ~$5k of fees round trip, held through the ~65-minute median window) but needs to be held for ONE pacing only. Effect: redemptions right after release receive 39,590 IMD for 50,000 imdUSD against 47,500 honest (payoutPrice 1.1998e18 at an attested 1e18), then 1.14, 1.083, 1.029, par after the fourth paced hour; the redemption floor cash enforces is about $0.79-0.83 instead of $0.95-0.995 through those hours, so imdUSD can trade down without redemption arbitrage, and every in-band candidate leaves the band for the same window. Nothing is taken (minGemOut lets a redeemer wait), so low: what is blocked is the peg defence, for about four hours per ~$5k push, repeatable. This is the manufactured version of the ACCEPTED honest-fall lag, which the accepted reasoning (an honest fall is rare) does not cover. Smallest fix: store the paced price's rise no faster than its fall is allowed (e.g. _pricePaced follows the attested price up by at most PAYOUT_PRICE_FALL_BPS_PER_HOUR per paced hour) while cash still pays at max(attested, paced): a real rise is still paid at once through the attested term, the redeemer is never overpaid (the attested price bounds the payout from below as now), and a one-window pump lifts the stored figure by at most 5%. Or state the bound at cash and on the risks page.

**Reproduction**

test/scratch/PumpRelease.t.sol (figures; passes on a fix that paces the rise). Same fixture as the high (par book: BOOK 199,000/99,500 at 200%, HOLDER 300,000/100,000, 24 hourly pacings, IMD at $1). Both feeds set to 1.20x, one block later vault.pace(): paced().price == 1.2e18. Both feeds back to 1.00x, one block later: payoutPrice() == 1.1998e18 against an attested 1e18 and HOLDER cash(50_000e18, 0, BOOK) returns 39,589.93e18 IMD. EXPECTED at the honest, fresh attested price with the 5% fee: 47,500e18. ACTUAL: 39,589.93e18 (17% fewer). Hourly pacings at 1.00x then read payoutPrice() 1.13981e18, 1.08282e18, 1.02868e18, 1.0e18 after one, two, three and four hours (test log).

### 3. Low: _tallyPrincipalRetired: a self-redemption of a one-block-old draw is booked as cancelling pre-existing principal, so a churner zeroes the paced debt (and backedDebt) every block while the book is unch

`src/CDPVault.sol:928`

```
        if (rest != 0) _transientAdd(cancellation ? CANCELLED_PRE_SLOT : WIPED_THIS_TX_SLOT, rest);
```

From audit_flow 5f94023e, reproduced. The cancellation-aware clamp nets a position's own principal out of a cancellation only through MINTED_BY_SLOT, which is transient and empties at the block boundary. Debt drawn one block earlier never entered the paced debt (the pacing before the draw wrote the figure; the follow step for 12 seconds is 0.03%), yet cancelling it in the next block counts in full as pre-existing principal cancelled (CANCELLED_PRE_SLOT) and _clampPacedDebt caps the paced debt at pacedAtStart - cancelled. So record 25's finding 2 is fixed for one transaction and reopens one block later: lock + draw X at block n, then cash(X, 0, self) + lock(the IMD paid) + draw(X) at block n+1 takes the paced debt from T to max(0, T + step - X) while totalDebt, the churner's loan and its collateral are unchanged (the 5% redemption fee stays in the churner's own position as collateral, b952037a). With X >= T the paced debt is 0, ParameterizedVault.backedDebt() is 0 and earnLine() falls to the reserve term, recovering at 10% of max(paced, 100,000) an hour from zero (10,000/h), so a $1M book takes about a day to count again and the churner repeats every block for gas plus attestations. Question 2's other parts, checked: the XOR-keyed slot cannot collide with a fixed slot or another owner's (the base's top 96 bits are random and owners differ in the low 160 bits); netting a position's own minted principal cannot cancel seasoned debt while reporting none within one transaction (own <= the tally the same transaction added); zero-second debt cannot count sooner than the follow rate in one transaction (_debtForPacing subtracts MINTED_THIS_TX_SLOT). Severity: WAGE_WAD is 0 at launch, so nothing is blockable with the constants as committed (the rating the sweep gave the same-transaction case); once governance sets a wage this is a gas-priced denial of the work channel's ratio term, and the ceiling as an aggregate is ACCEPTED. Smallest fix: either accept and state it (the NatSpec at 898-902 and 971-976 says only that the transaction's own fresh draw moves nothing, which is literally true), or count a cancellation against the paced debt only up to the position's principal older than the current follow window (e.g. keep per position in storage the principal minted since the last pacing and net it out as MINTED_BY_SLOT does, decayed at the follow rate).

**Reproduction**

test/scratch/CrossBlockSelfRedeem.t.sol. Fixture as the high's (IMD $1, NHI 0.85). BOOK locks 199,000 / draws 99,500; 24 hourly pacings; backedDebt() == 99,500e18. CHURN (200,000 IMD) calls lock(200_000e18) then draw(100_000e18) at block n: paced().debt stays 99,500e18. At block n+1 CHURN calls cash(100_000e18, 0, CHURN), lock(the 95,000 IMD paid) and draw(100_000e18). One block later: CHURN's position is 200,000 collateral / ~100,000 debt, totalDebt ~199,500e18 (unchanged but 12 seconds of fee), but paced().debt == 0 and backedDebt() == 33e18 (one block of the 10,000/h recovery). EXPECTED: the seasoned 99,500 untouched, so the paced debt and backedDebt >= 99,500e18. ACTUAL: 0. Fails with 'self-redemption of one-block-old debt lowered the paced debt below the seasoned book: 0 < 99500000000000000000000'.

### 4. Low: DeployMainnet.verifySeeded bounds the NHI first value only by <= 1e18: a wrong first NHI deploys an immutable vault at mat 200 with zero grace and takes days of daily epochs to walk back

`script/DeployMainnet.s.sol:433`

```
        require(nhi <= 1e18, "seeded: NHI above one");
```

From audit_math 604c76c3, confirmed by reading. runVault runs verifySeeded, which checks the pool against REFERENCE_IMD_ETH_WEI, the price and spot feeds against the pool and each other, and NHI only for being at most one. A feed's first value is bounded by nothing on chain (SwarmFeed: the first value anchors the first epoch), the relay is permissionless, and the runbook's step 2 relies on the operator reading the NHI figure by eye. An NHI first value at or below 0.6e18 passes every require and the vault opens at mat() 200 and lull() 0 (CDPVault._mat/_lull): bark and bite land in the same transaction with no grace and every position needs 200% rather than 170%. Because NhiFeed's epoch is a day with a 20% allowance (NHI_MAX_AGE 1 days, FEED_MAX_DEVIATION_BPS 2000, _checkValue against the epoch's anchor), walking 0.5 back to 0.85 takes three daily epochs at best (0.5 -> 0.6 -> 0.72 -> 0.85), and the keeper's own guard (runbook 7.4a) refuses to buy an answer the feed would refuse, so nothing shortens it; the vault is immutable and nothing in the deployment refuses the value. Question 6's other deployment items, checked: _refuseAnotherVault, the salt through the private relay and verify are as record 22 left them; the first price and spot values are bounded by the reference and the pool. Smallest fix: give verifySeeded a REFERENCE_NHI env like REFERENCE_IMD_ETH_WEI and require the seeded NHI within the same band of it, and for launch require nhi > 0.6e18 so the vault cannot open with zero grace; have the runbook's step 2 state the NHI check explicitly.

**Reproduction**

Read script/DeployMainnet.s.sol:424-434: the only NHI require is nhi <= 1e18 (the price and spot values get three band checks each). State: stage one deployed; the first NHI attestation relayed carries 0.5e18 (a partial-day read, or the wrong unit); price and spot honest. `REFERENCE_IMD_ETH_WEI=<market> forge script script/DeployMainnet.s.sol --sig verifySeeded()` passes every require (0.5e18 <= 1e18) and prints 'Seeded and verified'; runVault deploys. EXPECTED per the runbook: a vault opening at mat 170 and six hours of grace. ACTUAL: vault.mat() == 200 (CDPVault._mat: nhi <= 0.6e18 returns 200), vault.lull() == 0 (_lull: nhi <= 0.6e18 returns 0); a first borrower at 190% is unhealthy, barkable and bitable in one block; the next NHI value above 0.6e18 reverts ExcessDeviation (20% of the 0.5 anchor) until the day-long epoch rolls.

### 5. Info: cash's @notice says imdUSD is burned 'for feed-priced IMD'; since 92b873b IMD is paid at the paced payout price, which can sit above the feed for five paced hours

`src/CDPVault.sol:706`

```
    /// @notice Burn exactly `amount` caller imdUSD for feed-priced IMD, less the capped fee, scaled down by
```

Merged from audit_math 58205bcd, audit_permissions 616fde7e, audit_economics 687e040f. gemOut = Math.mulDiv(amount, payoutScale, payPrice) with payPrice = _payoutPrice(price) = max(attested price, paced price) (lines 727 and 763), so after any fall of the attested price the IMD is priced above what the feed reports for up to five paced hours (20%) or ten (40%). The @notice is the line integrators read: one quoting a redemption from collateralPriceFeed().latestValue() instead of payoutPrice() overstates gemOut by up to 25% during that lag and gets MinimumOutNotMet. The @dev block and the comment at 720-726 describe the paced price correctly. Related statements of the same mechanism that are off: the 'four paced hours' in docs/AUDIT-FINAL-SWEEP-2-2026-10-09.md's resolution (0.95^4 = 0.8145 > 0.80; the fifth pacing reaches a 20% fall) and the 'no longer pays' claim at lines 349-353 and docs/MAINNET-RUNBOOK.md:413-415, which the high finding covers. Every other comment checked in scope matches the code: the transient slot constants equal keccak256 of the strings their comments name, the XOR-keyed slot's top 96 bits differ from every fixed slot's, the initcode is 47,867 bytes ('within about 2 KB' of 49,152 holds at a 1,285-byte margin), the compounding wording at ParameterizedVault 238-241 and 263-266 is correct, and the TransientReentrancyGuard's slot and semantics match its NatSpec (per-contract transient flag, nested guarded call reverts, equivalent to OpenZeppelin's for every reentrant path including one from another guarded contract in the same transaction, since tstore is per address). Fix: 'for IMD at the higher of the attested price and the paced payout price (`payoutPrice`)'.

**Reproduction**

Read src/CDPVault.sol:706 against lines 727 and 763. Fixture of the high: both feeds set to 0.8x and cash(50_000e18, 0, BOOK) the next block: payoutPrice() reads 1.0e18 (the feed reads 0.8e18) and gemOut = 50,000 x 0.95 / 1.0 = 47,500e18, where a feed-priced payout would be 59,375e18; after four hourly pace() calls payoutPrice() is 0.81437e18 > 0.8e18 and only the fifth brings it to the feed (test/scratch/Proof_05be3eba554b.t.sol, test/scratch/PumpRelease.t.sol logs).

---

Judge's submission `12ab46c1c278ad515ec2338a416dd722dd79f3dc5fac73449e60756c2766c7ef`, 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.
