# Audit report

> Audit the whole system: src/, script/DeployMainnet.s.sol and deploy/mainnet/, at the pinned commit, for a mainnet launch. Nine audit rounds and their fixes are already in (docs/AUDIT-*.md). The newest is docs/AUDIT-FINAL-VAULT-PANEL-2026-10-08.md, whose fixes are this commit (6085c8a); its Resolution section says how each finding was answered. Since the last whole-system sweep (973369e, docs/AUDIT-SWEEP-PANEL-*-2026-10-07.md) only src/CDPVault.sol and src/ParameterizedVault.sol changed: git diff 973369e 6085c8a -- src. A finding of an earlier round counts only if its fix regressed or left a gap. Two items are accepted with their reasons stated where they live, and are findings only if the reason is wrong: the repay-then-redeem premium (CDPVault._backingPerUnit) and a redraw after a redemption releasing a repayment's fee share early (CDPVault._lag).
>
> 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 VAULT'S NEWEST LINES (git diff d7fceab 6085c8a -- src). (a) The excess of 58f73de is removed: confirm nothing of it remains, that the same-call repayment tally (REPAID_THIS_TX_SLOT, added in _payDebt and cover, not cash) closes the same-transaction wipe, cash, draw, and that the accepted cross-transaction premium is bounded as stated. (b) The quiet-day cutoff in _cool now applies to the vault totals, the positions and the banks alike: prove no sequence lets one position's capital warm another's, and that what remains (a stale position reading zero while a busy total keeps its share) only understates the lagged figures. (c) The fee base (_feeBase) is floored at 1,000 imdUSD (_feeBaseFloor) and read once per redemption, before the candidate is touched: can the floor be used to dilute fees once the protocol is past it, or the single read to misprice the stored rate?
> 2. CROSS-SUBSYSTEM SEQUENCES. Any sequence across the vault, the Treasury, Parameters, the feeds, OracleAsker and SwarmRelay (bundled through relayMany, relayAndBark, relayAndBite or a contract of your own) that extracts value, mints unbacked imdUSD, blocks liquidation or redemption, or desynchronises an accounting record. Consider transaction boundaries explicitly: what the vault tallies per transaction (debt at transaction start, secured and minted, work minted, repaid) and what it keeps per position across transactions (cold capital, banks, feeExcess).
> 3. THE ORACLE AS AN ATTACK SURFACE ON THE VAULT: with the per-epoch deviation bound, silence measured from the relay, the Treasury's refresh rules and the two-stage deploy as committed, the cheapest profitable manipulation of collateral prices (over-borrowing) or NHI (liquidation timing), in money and hours, at LINE $1M.
> 4. LIVENESS AND THE LAUNCH: every way the protocol can halt and how each recovers, with docs/MAINNET-RUNBOOK.md and docs/PARAMETERS-2026-10-05.md checked against the code; the first hours after launch, when most supply is new (the lag, the fee base and its floor, the oracle's day-one funding through fundOracle and the keeper's fallback).
> 5. THE DEPLOYMENT: DeployMainnet.run (stage one), verifySeeded (against the pool and REFERENCE_IMD_ETH_WEI), runVault (stage two), verify, deploy/mainnet/plan.py and the pinned bodies. What can still be deployed wrong and pass, and what can happen between the two stages? Contract size: ParameterizedVault's initcode is 46,662 of 49,152 bytes.
> 6. Every comment or NatSpec in src/ that claims a property the code does not have.
>
> 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 | `6085c8adb89d382b324c12b46ac22ba99a15c510` |
| Job | `08a12413-5e3b-4baf-9f1c-6d1ba99e9487` |
| Judged | 2026-10-08 10:26 UTC |
| Findings | 3 low · 2 info |

Four agents audited the code as it is at `6085c8a`, 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. Low: CDPVault._feeBaseFloor: the 1,000 imdUSD floor closes the launch pin for dust only; a 90 imdUSD reserve-funded burn still stores the 4.5% cap as everyone's base rate (final vault panel #6 marked Fixed

`src/CDPVault.sol:1057`

```
        return 1_000e18;
```

Q1(c) and Q4 (the first hours after launch). _feeBase() returns max(warm base, 1_000e18) and _redemptionRate() measures the increase as amount / prior / redemptionDivisor (line 891). With the launch divisor of 2, any burn of at least 1,000 x 0.045 x 2 = 90 imdUSD against the floored base is the whole 4.5% cap, and cash() stores `base` unchanged whenever the burn has no fresh part (line 727), which every reserve-funded burn lacks (principalCancelled = 0, so freshCancelled = 0). The reserve route is open to anyone: redemptionReserve is gem.balanceOf(treasury), listed or not (ParameterizedVault line 157), so a transfer of a few sIMD to the Treasury opens it, the first liquidation cut opens it too, and the burn is paid back in sIMD less the fee. So while the warm supply is under 1,000 imdUSD (about five minutes after a 100,000 draw, about an hour after a 10,000 draw, i.e. the first hours after launch) a 90 imdUSD redemption pinned through the reserve stores 0.045e18 as redemptionBaseRate for everyone, for 4.5 imdUSD of fee; the honest increase for 90 of a 100,000 supply is 90 / 100,000 / 2 = 4.5 bps. The stored rate decays with a twelve-hour half-life while the cold supply warms with a six-hour one, so the pin outlives the condition that produced it: the 1 imdUSD quote reads 500 bps at once, 369 at +6 h, 276 at +12 h, 210 at +18 h (when 7/8 of the supply is warm and the honest increase is under a basis point), 163 at +24 h, 79 at +48 h. The resolution of final-vault-panel #6 says the floor means 'a dust redemption while most supply is new cannot store the cap for everyone': true for dust, not for 90 imdUSD, and the NatSpec at lines 1043-1046 presents the item as closed. Not an extraction (the pinner pays the cap on its own burn): the throttle everyone after pays is set to its maximum on demand for about $4.50 while the protocol is young, so the peg floor min(1 - fee, backing) sits at 0.95-0.98 for two days instead of 0.995, and redemptions, the peg's defence, are deterred when most supply is new. Reachable with the constants as committed (LINE $1M, divisor 2, wage 0) by anyone holding 90 imdUSD; repeatable while the warm supply is below 1,000. Answer to the rest of Q1(c): once the warm base is past the floor the floor is inert (a max) and cannot dilute anything, and the single pre-touch read of `prior` makes the quote and the stored rate agree. Smallest fix (fits the 2,490-byte initcode margin): bound the STORED increase by the burn's share of the live supply while still charging the redeemer against `prior`, e.g. in cash(): redemptionBaseRate = min(freshCancelled == 0 ? base : _redemptionRate(amount - freshCancelled, prior), decayedRedemptionBaseRate() + mulDiv(amount, 1e18, stablecoin.totalSupply()) / redemptionDivisor()); or raise _feeBaseFloor to a figure a pinner cannot cheaply clear (e.g. 100,000e18, which makes the launch pin cost 9,000 imdUSD of burn) and say so in docs/MAINNET-RUNBOOK.md section 7 (open redemptions a day after the first draws). Merged from audit_flow (low), audit_permissions (low) and audit_economics (low), all three reproducing the same mechanism; the attached proof is audit_flow's, run here and failing as stated.

**Reproduction**

test/scratch/Proof_FloorPin.t.sol (attached; fails on 6085c8a with 'a 90 imdUSD burn must not store the cap as everyone's base rate: 45000000000000000 >= 45000000000000000'). ParameterizedVault over an 18-decimal MockIMD at $1 (IMD/ETH 1/2000 times a Chainlink ETH/USD of 2000e8 etched at CHAINLINK_ETH_USD), NHI 0.85 (mat 170), TreasuryFactory etched at TREASURY_FACTORY, Treasury given 1,000 IMD so the burn is reserve-funded. BORROWER lock(400_000e18), draw(100_000e18), hands PINNER 90 imdUSD; 12 seconds later (warm supply about 38.5 imdUSD, base = the 1,000 floor, redemptionFeeBps(90e18) == 500) PINNER cash(90e18, 0, address(0)) is paid 85.5 IMD. EXPECTED: redemptionBaseRate well under the 0.045e18 cap (the honest increase for 90 of a 100,000 supply is 4.5e14) and redemptionFeeBps(1e18) near the 50 bps floor 18 hours later. ACTUAL (test/scratch/FloorPinLog.t.sol, logs): redemptionBaseRate() == 45000000000000000 (the cap); redemptionFeeBps(1e18) == 500 at once, 369 at +6 h, 276 at +12 h, 210 at +18 h, 163 at +24 h, 79 at +48 h. Control: the committed test/final-vault-panel/FeeBaseFloor.t.sol shows 5 imdUSD stores 25 bps; 90 is the threshold at divisor 2 (45 at divisor 1).

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

// Whole-system audit at 6085c8a, Q1(c): the fee-base floor (CDPVault._feeBaseFloor, 1,000 imdUSD) closes the
// launch pin for DUST only. At divisor 2 any reserve-funded burn of 90 imdUSD or more against the floored base
// (90 / 1,000 / 2 = 4.5%) still STORES the cap as redemptionBaseRate for everyone, for 4.05 imdUSD of fee.
// Fails on 6085c8a: redemptionBaseRate() == 0.045e18 after the burn and the 1 imdUSD quote 18 hours later
// reads 210 bps where the honest increase is under a basis point.

import {Test} from "forge-std/Test.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 FpFeed is ISwarmFeed {
    uint256 public constant maxAge = 1 days;
    uint256 private value;
    uint64 private updatedAt;

    constructor(uint256 v) {
        value = v;
        updatedAt = uint64(block.timestamp);
    }

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

    function isStale() external pure returns (bool) {
        return false;
    }
}

contract FpMirror is ISwarmFeed {
    ISwarmFeed private immutable primary;

    constructor(ISwarmFeed p) {
        primary = p;
    }

    function latestValue() external view returns (uint256, uint64) {
        return primary.latestValue();
    }

    function isStale() external view returns (bool) {
        return primary.isStale();
    }

    function maxAge() external view returns (uint256) {
        return primary.maxAge();
    }
}

contract FpAggregator {
    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 ProofFloorPinTest is Test {
    address private constant BORROWER = address(0xB0B);
    address private constant PINNER = address(0x919);

    MockIMD private imd;
    ParameterizedVault private vault;
    ImdUSD private stable;

    function setUp() public {
        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);
        vm.etch(CHAINLINK_ETH_USD, address(new FpAggregator()).code);
        vm.warp(1_000_000);
        imd = new MockIMD();
        FpFeed primary = new FpFeed(uint256(1 ether) * 1e18 / 2000 ether); // IMD = $1
        FpFeed health = new FpFeed(0.85 ether); // mat 170
        vault = new ParameterizedVault(
            address(imd), address(0), address(0), address(primary), address(health), address(new FpMirror(primary))
        );
        stable = vault.stablecoin();
        address treasury = address(vault.treasury());
        vm.startPrank(APPROVED_OPERATOR);
        imd.mint(BORROWER, 400_000 ether);
        imd.mint(treasury, 1_000 ether); // the burn is reserve-funded: no fresh part, the quote is stored unchanged
        vm.stopPrank();
    }

    function test_ninetyImdUsdReserveFundedBurnStoresTheCapForEveryone() public {
        // Launch: the first loan is all the supply there is, and all of it is cold.
        vm.startPrank(BORROWER);
        imd.approve(address(vault), type(uint256).max);
        vault.lock(400_000 ether);
        vault.draw(100_000 ether);
        stable.transfer(PINNER, 90 ether);
        vm.stopPrank();
        vm.warp(block.timestamp + 12);
        // Warm supply is about 38.5 imdUSD, so the base is the 1,000 floor: 90 / 1,000 / 2 is the whole cap.
        vm.prank(PINNER);
        vault.cash(90 ether, 0, address(0));
        uint256 cap = (vault.REDEMPTION_FEE_CAP_BPS() - vault.REDEMPTION_FEE_FLOOR_BPS()) * 1e14;
        assertLt(vault.redemptionBaseRate(), cap, "a 90 imdUSD burn must not store the cap as everyone's base rate");
        // Eighteen hours later 7/8 of the supply is warm and the honest increase for 1 imdUSD is under a basis
        // point, so an honest quote is the 50 bps floor plus at most a few bps.
        vm.warp(block.timestamp + 18 hours);
        assertLt(vault.redemptionFeeBps(1 ether), 100, "the stored pin must not still price a 1 imdUSD burn above 1%");
    }
}
```

### 2. Low: DeployMainnet: the vault is not absent between the two stages; anyone can CREATE2 ParameterizedVault at the planned address from the public salt and initcode as soon as stage one lands, so a raced fir

`script/DeployMainnet.s.sol:212`

```
        _deploy(SALT_VAULT, _vaultInit(p), p.vault);
```

Q5 (what can happen between the two stages). The split into run() and runVault() exists so that 'with the vault absent until the first values are checked, a raced first value can price nothing' (lines 182-184; docs/MAINNET-RUNBOOK.md section 7, paragraph 2; src/SwarmFeed.sol 238-239 'before deposits open'). Nothing enforces the absence. _deploy sends `salt ++ initcode` to CREATE2_FACTORY (0x4e59b44847b379578588920cA78FbF26c0B4956C, the canonical permissionless deterministic-deployment proxy), whose resulting address is a pure function of (salt, initcode) with no dependence on the caller. SALT_VAULT (line 96) is a literal in the committed script and _vaultInit(p) (lines 485-492) is type(ParameterizedVault).creationCode ++ abi.encode(STAKED_IMD, 0, WORK_ORACLE_SENTINEL, p.price, p.nhi, p.spot), all public once the deploy commit is published and reproducible from the verified feed bytecode. The vault constructor's only external prerequisites (STAKED_IMD, the three feeds, TREASURY_FACTORY and WORK_ORACLE_FACTORY having code) are met the moment stage one's last transaction lands, and nothing in the constructor chain checks who triggered it: ParameterizedVault, TreasuryFactory.create, WorkOracleFactory.create (SwarmWorkOracle accepts any contract creator since the 2026-10-05 fix), Parameters, ImdUSD, UsdPriceFeed and SharePriceFeed are all caller-agnostic. So after run(): (1) a stranger pumps IMD's pool, buys price and spot attestations of it and relays them through SwarmRelay (a feed's first value is bounded by nothing on chain) plus an honest NHI; (2) sends SALT_VAULT ++ _vaultInit(p) to the proxy (about 13.8M gas, under the 16.7M EIP-7825 cap), which lands the vault at exactly p.vault, live from its constructor; (3) locks sIMD and draws against the pumped value in the same block, before verifySeeded ever ran (it is a view the operator runs before THEIR broadcast). The operator's runVault() then fails verifySeeded, or, with honest first values and a one-wei draw, _deploy prints 'exists, skipped' (lines 251-254) and verify() reverts on totalSupply() == 0 / totalDebt() == 0 (lines 278, 307): the planned address, the keeper's configuration and the announcement are burnt and the vault must be re-salted, and since every salt is in the committed script the same race applies to the next one. Impact: no user funds (nothing is announced yet) and the attacker cannot profit (there is no imdUSD market; it can wipe and free at the pumped price and lose only gas and pool fees), so this is griefing that forces a redeploy per attempt for about 14M gas, or real bad debt only if the operator proceeds past a failed verify. A stranger who deploys it with honest values and touches nothing produces the correct vault early, which verify() passes: harmless. Reachable with the script as committed on mainnet, no key needed. Smallest fix: the vault's address is read by nothing as a source constant (it is layer 5; only the record and the keeper read it from deployment.json), so it need not be planned: deploy it with plain CREATE from the throwaway deployer in runVault() (`new ParameterizedVault(STAKED_IMD, address(0), WORK_ORACLE_SENTINEL, p.price, p.nhi, p.spot)` inside the broadcast, record the address instead of computing it; _record already handles a vault address known only after the broadcast), which no one else can front-run; or keep CREATE2 but through a deployer that binds the salt to msg.sender (ImmutableCreate2Factory-style, the first 20 bytes of the salt equal to the caller). Either way reword lines 178-184, docs/MAINNET-RUNBOOK.md section 7 paragraph 2 and src/SwarmFeed.sol 238-239 to what the stage split actually buys: a check the operator runs before THEIR deploy. Merged from audit_math (low), audit_flow (low), audit_permissions (low) and audit_economics (medium); rated low here because the operator detects it in verifySeeded/verify and the loss is a re-salt, not funds.

**Reproduction**

test/scratch/VaultFrontRun.t.sol (passes, demonstrating the sequence; gas 13,822,548). Stage one as deployed: SwarmRelay etched at ATTESTATION_RELAYER, WorkOracleFactory at WORK_ORACLE_FACTORY, TreasuryFactory at TREASURY_FACTORY, PriceFeed/NhiFeed/SpotFeed deployed unseeded with (PRICE_MAX_AGE/NHI_MAX_AGE/SPOT_MAX_AGE, 2000); a MockIMD stands in for STAKED_IMD, which has no code off mainnet; the canonical proxy is present in the test EVM. init = type(ParameterizedVault).creationCode ++ abi.encode(gem, 0, WORK_ORACLE_SENTINEL, price, nhi, spot); planned = vm.computeCreate2Address(keccak256('infer-protocol/mainnet/v1/ParameterizedVault'), keccak256(init), 0x4e59b4...956C), the script's own _at(). From address(0xBAD): CREATE2_FACTORY.call(bytes.concat(SALT_VAULT, init)). EXPECTED (the comment at 182-184): no vault can exist before runVault. ACTUAL: the call succeeds, returns exactly `planned`, planned.code.length == 22,449, ParameterizedVault(planned).treasury().vault() == planned and priceFeed() == price; DeployMainnet._deploy would then log 'exists, skipped' for it. On a fork after stage one the same is `cast send 0x4e59b44847b379578588920cA78FbF26c0B4956C $(cast concat-hex <SALT_VAULT> <forge inspect ParameterizedVault bytecode ++ abi.encode(STAKED_IMD, 0, WORK_ORACLE_SENTINEL, priceFeed, nhiFeed, spotFeed)>)` from any account.

### 3. Low: CDPVault._backingPerUnit: the lagged figure credits the whole reserve against the warm supply alone, so while most supply is fresh (the first hours after launch) it exceeds par and a newcomer's one-tr

`src/CDPVault.sol:775`

```
            uint256 lagged = Math.mulDiv(reserve + _securedCollateralValue(price, true), 1e18, supply - fresh);
```

Q4 (the first hours after launch) and Q2 (a D1-shaped sequence across the vault and the Treasury). The lagged figure is (reserve + warm secured collateral) / (supply - cold principal). Cold capital leaves both sides, but the reserve is not capital a position brought and is not scaled: it is counted in full against the WARM supply only, although it backs every unit. While the warm supply is small relative to the reserve (right after launch, when the whole book is hours old) the lagged figure exceeds par, so _backingPerUnit returns min(live, lagged) = the LIVE figure, and the live figure counts a newcomer's collateral held across one transaction (SECURED_THIS_TX_SLOT and MINTED_THIS_TX_SLOT exclude it only inside the transaction that brought it). A newcomer can therefore lock and draw (cold) in one transaction, cash from the reserve in the next at par where the honest post-exit backing is below it, then wipe and free: the D1 round trip, which the lag closes once the warm supply is large, is open for about half a day after launch whenever the Treasury holds sIMD and the book has fallen below par. The gain is (paid - honest) x the reserve-funded amount, at most the reserve x the gap below par less the redemption fee; it needs a Treasury sIMD balance (the runbook does not seed one: its only sIMD income is liquidation cuts, cover sweeps and donations) and a below-par book within the first hours, which is why this is low. The NatSpec at lines 752-755 ('An attacker's capital can raise the live figure but not the lagged one ... When every unit of supply is fresh there is no lagged figure and the live one stands') describes the all-fresh case exactly and misses the nearly-fresh one, where the lagged figure exists but is vacuous. Reachable with the constants as committed. Smallest fix, no new storage: apportion the reserve to the warm supply pro rata in the lagged figure, `Math.mulDiv(reserve, supply - fresh, supply)`, which is exact once the launch supply is warmer than the newcomer's (at 6 hours it reads 0.90 against the honest 0.95, the lag's underpaying direction, instead of 1.0) and still vacuous only when the newcomer's capital is as fresh as everything else (the first minutes); and operationally keep the Treasury's sIMD out of the reserve until the launch supply has warmed (a day) and say so in docs/MAINNET-RUNBOOK.md section 7. From audit_economics (low), reproduced; no proof attached because the complete fix is partly operational.

**Reproduction**

test/scratch/LaunchReserve.t.sol (passes, logs). ParameterizedVault over MockIMD at $1 (Chainlink 2000e8 etched), NHI 0.85, wage 0. OTHER lock(170_000e18), draw(100_000e18) and hands ATTACKER the 100,000 imdUSD; the Treasury is given 20,000 IMD; IMD to $0.50 (OTHER at 85%); 12 s later backingPerUnit() == 0.95e18 (honest: (20,000 + 170,000) x 0.5 / 100,000). At each age of the launch supply (12 s, 1 h, 6 h, 12 h, 24 h; snapshot and revert between): ATTACKER lock(400_000e18), draw(100_000e18); 12 s later read backingPerUnit(), the figure a reserve-funded cash is paid against. EXPECTED: at most 0.950005e18 (the newcomer's capital is one transaction old and leaves in the next). ACTUAL: 1.0e18 at 12 s, 1 h and 6 h; 0.983838e18 at 12 h; 0.950404e18 at 24 h. At 12 s ATTACKER cash(5_000e18, 0, address(0)) is paid 9,500e18 raw IMD from the Treasury (par less the 5% fee) where the honest figure is 9,025e18: the reserve leaves at par while the book is backed at 0.95.

### 4. Info: CDPVault._cool NatSpec: the accepted 'stale position reads zero while a busy total keeps its share' gap understates laggedNow() but, on the debt side alone, RAISES the lagged backing figure in _backin

`src/CDPVault.sol:1019`

```
    /// until it cools. That only understates the lagged figures, the safe direction: accepted (retry2, low).
```

Q1(b) and Q6. The claim holds for laggedNow(): a position untouched for BACKING_WARMUP reads zero while the total, kept busy by others, still holds its share, so lagDebt and lagSecured are both understated. For _backingPerUnit the two sides pull in opposite directions: lagged = (reserve + min(lagSecured-bound, lagDebt-bound x mat)) / (supply - fresh) with fresh = totalDebt - lagDebt, so an orphan on the DEBT side alone shrinks the denominator and raises the figure, while an orphan on the SECURED side lowers the numerator. A debt-only orphan arises from any draw by a position in the 170-200% band (its term stays its collateral, so only coldDebt grows), left untouched for a day while others keep the total busy. The raised lagged figure matters only when it is also the minimum, i.e. when some other cold collateral has lifted the live figure above it; then redeemers are paid against supply - orphan instead of supply. Bounded: the orphan is at most the position's day-old cold debt times 2^-4 and keeps halving, and a band position's debt-only draw is at most 17.6% of its principal, so the overstatement is under (0.176 x P / 16) / supply, far below the accepted repay-then-redeem premium and not exploitable above the redemption fee. Reported because Q1(b) asks whether the remainder 'only understates' and the comment and the final-vault-panel #4 resolution say so. Smallest fix: none needed for safety; reword line 1019 to 'understates laggedNow(); in the backing cap a debt-side orphan can overstate the lagged figure by at most a sixteenth of the stale position's cold principal over the supply, accepted'. From audit_math (info), reproduced.

**Reproduction**

test/scratch/OrphanDirection.t.sol (passes, logs). ParameterizedVault over MockIMD at $1 (Chainlink 2000e8 etched), NHI 0.85. U locks 17,000 and draws 10,000; A locks 4,000 and draws 1,000; two days; IMD to $0.50 (U 85%, A 200%); both lock(1); two more days: backingPerUnit() == 954545454545454545, laggedNow() == (11,000e18, 21,000e18 + 1). A draws 150 (debt-only: its term stays 4,000). T (lock 100) draws 1 every 6 hours for a day to keep the total busy; +1 s: laggedNow() == (11142.75e18, 21008.5e18), total cold debt 11.25e18 (A's 150/16 plus T's 1.875) with A's own cold reading zero; backingPerUnit() == 942083557468172852 (the live figure binds: the honest value with A fully warm). U then locks 2,000 (cold secured only, raising the live figure). EXPECTED: the lagged minimum at or below the honest 0.942083e18. ACTUAL: backingPerUnit() == 942698147227049462, the lagged figure computed over supply - 11.25 instead of supply, 0.065% ABOVE the honest value.

### 5. Info: Comments and NatSpec that claim properties the code does not have at 6085c8a; the answers to Q1-Q6 where nothing is wrong; coverage

`src/CDPVault.sol:753`

```
    /// figure but not the lagged one, whichever position it sits in (`_lag`); honest redemptions are not
```

CLAIMS WITHOUT THE PROPERTY (Q6), each verified by reading the cited lines. (1) CDPVault 752-754, 'An attacker's capital can raise the live figure but not the lagged one ... honest redemptions are not underpaid': while most supply is fresh the lagged figure is vacuous and the live one, which the newcomer's capital raises, stands (low finding, line 775); and two accepted items in the same file underpay honest redemptions for hours (325-327, a price fall's upward re-pricing counts as cold; 1017-1019, a stale position's share orphaned in the total). True only of the mechanism it names. (2) CDPVault 1019 'only understates the lagged figures': a debt-side orphan overstates the backing cap (info, line 1019). (3) script/DeployMainnet.s.sol 182-184 ('With the vault absent until the first values are checked, a raced first value can price nothing'), src/SwarmFeed.sol 238-239 ('verifySeeded checks it against the pool before deposits open') and docs/MAINNET-RUNBOOK.md 321-324: the absence is not enforced (low finding, DeployMainnet 212). (4) CDPVault 1043-1046 and the final-vault-panel #6 resolution ('a dust redemption while most supply is new cannot store the cap for everyone'): true of dust; 90 imdUSD still does (low finding, line 1057). (5) CDPVault 183-184, Parameters 110-112 and DeploymentConfig 127-128, 'raises the fee base by redeemed / supply / divisor' ('At 1 a run reaches the 5% cap after 4.5% of supply'): since retry2 the denominator is the WARM supply plus warm repayments, floored at 1,000 (_feeBase, 1047-1053), not the live supply, and 'fee base' now names the denominator rather than the base RATE; at launch 45 imdUSD reaches the cap at divisor 1. (6) src/Treasury.sol 501-503, fundOracle: 'sIMD's one-block hold applies ... so a call right after a liquidation reverts and succeeds a block later': lines 553-558 catch the refused unwrap (try this.unwrapForOracle{gas: UNWRAP_GAS} ... catch { fromShares = 0; }) and the call returns the plain IMD sent, or 0, without reverting; the notice describes the pre-sweep-panel behaviour. (7) src/DeploymentConfig.sol 36-37, 'Treasury-funded staleness asks apply to [NHI] alone': OracleAsker.ask line 162 pays for ANY feed that is wideOpen (silent a lifetime with the allowance at WIDE_ALLOWANCE_BPS), price and spot included, as DeploymentConfig 241-249 and docs/PARAMETERS-2026-10-05.md say; the earlier comment in the same file is the stale one. (8) CDPVault 589-591, in cover: 'holding cover off costs the griefer the whole re-lock, with no liquidator needed', unqualified; the qualification the final-vault-panel resolution says was added (only while the Treasury holds imdUSD worth the re-lock; with less, CoverBelowCollateralValue or the burn reverts and the re-lock waits for bark, grace and bite) was written at _coverDust 655-656 only. (9) CDPVault 665, cash's @notice 'for feed-priced IMD, less the capped fee': the payout is also scaled by _backingPerUnit below par (line 711); the @dev says so, the @notice does not. (10) CDPVault 762, the accepted premium's parenthetical '15% of it at a 170% minimum ratio': the bound formula (supply - fresh) / (supply - fresh - repaid) with repaid at most the principal above half the collateral's value is correct, so the acceptance's reason stands; but the premium exists only below par, where a dominant churner sits below mat (wipe has no health check), and there the repayment is up to half the principal at 100% and 60% at 80%: test/scratch/PremiumUnderwater.t.sol, X and U each 10,000 IMD / 5,000 imdUSD, IMD to $0.40 (both 80%, backingPerUnit 0.8e18), honest cash(1,900e18, 0, X) pays 3,610e18 raw IMD; after X wipe(3,000e18) (W = 5,000 - 10,000 x 0.4 / 2, term unchanged) the figure reads 1e18 (capped) and the same cash pays 4,512.5e18, +25%, where a 15%-of-principal bound predicts at most +8%. State the crash-regime figure at the line. (11) CDPVault 330, 'cold capital, and a bank, left untouched this long count in full': for cold capital the cutoff credits it in ful

**Reproduction**

(1) read CDPVault 752-755 against the low at 775 and the accepted items at 325-327 and 1017-1019. (2) test/scratch/OrphanDirection.t.sol. (3) test/scratch/VaultFrontRun.t.sol. (4) test/scratch/Proof_FloorPin.t.sol and FloorPinLog.t.sol. (5) read CDPVault 183-184, 885-893, 1047-1053; Parameters 110-112; DeploymentConfig 127-128. (6) read Treasury 501-503 against 553-558: `try this.unwrapForOracle{gas: UNWRAP_GAS}(IERC20(token), fromShares) {} catch { fromShares = 0; }` then `sent = plain + fromShares; if (sent == 0) return 0;`, no revert path for the hold. (7) read OracleAsker 162: `forStaleness = (f.keepAlive && nearStale(feed)) || wideOpen(feed)`, wideOpen true for a price feed silent ten hours. (8) compare CDPVault 589-591 with 655-656. (9) read 665 against 711-712. (10) test/scratch/PremiumUnderwater.t.sol (logs: honest 3610e18, churned 4512.5e18, premium 2500 bps). (11) read 330 against 971-975 and 1023. (12) read ParameterizedVault 155-158 and Treasury.fundOracle 531-535. (13) read tail() 1328. (14) _feeBaseFloor returns 1_000e18, so `prior` at 891 is never zero. (15) compare docs/MAINNET-RUNBOOK.md 243 with DeploymentConfig 258-259, OracleAsker 362-372 and DeployMainnet 322. (16) compare docs/MAINNET-RUNBOOK.md 345-346 with 352-353 and Treasury 531-535.

---

Judge's submission `7758891ff479ab74fb08620c8e41a607dcea8c323597716b0f57a7113c48206b`, 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.
