{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"08a12413-5e3b-4baf-9f1c-6d1ba99e9487","kind":"audit","nodes":[{"acceptedSubmissionHash":"e05d723760896697b6c4a5b37de15eb822acb440e4de36708791f44ec42c4158","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"d41577c5b95f9f7d0edc2599daa6dc5f0f6e549de4708c27237e12749591cc82","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"7758891ff479ab74fb08620c8e41a607dcea8c323597716b0f57a7113c48206b","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"8001dd351874f80a5f54e3231903526de4eb9496645c86933f4d1a67979e2a3d","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"3b898475802ac7311dfa249a472f2d742d653dd6a385d9a090ecf54f76416ba8","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"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).\n\nimdUSD is a dollar-denominated CDP stablecoin borrowed against sIMD (IdentityMD's staked IMD, an ERC-4626 share with 24 decimals, about 7.95 IMD each). Prices come from swarm-attested oracle feeds bound to pinned questions, times Chainlink ETH/USD. Everything about the deployment is in src/DeploymentConfig.sol and docs/MAINNET-RUNBOOK.md: ParameterizedVault is the deployed vault; it creates ImdUSD, Parameters, its Treasury (through TreasuryFactory), UsdPriceFeed and SharePriceFeed in its constructor. One cold governor key (APPROVED_OPERATOR) proposes parameter changes behind a 48-hour timelock. Collateral pricing is per 1e18 raw units throughout. IMD's only market is a full-range Uniswap v4 pool, about $2.3M a side with a 1% fee; docs/PARAMETERS-2026-10-05.md has the numbers every economic parameter was chosen from.\n\nAnswer each numbered question, including the ones where nothing is wrong:\n1. 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?\n2. 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).\n3. 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.\n4. 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).\n5. 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.\n6. Every comment or NatSpec in src/ that claims a property the code does not have.\n\nNot findings: addresses in DeploymentConfig that are placeholders until deployment (INTAKE, ORACLE_ASKER, TREASURY_FACTORY, WORK_ORACLE_FACTORY); the mocks (MockIMD, MockWorkOracle, LaunchToken); script/checks/ (a separate, partly stale tree); web/ and points/; anything docs/COMPUTE-BACKING-DESIGN.md describes as future work; and findings of the earlier audits in docs/AUDIT-*.md and docs/INTERNAL-AUDIT-2026-10-04.md, unless the fix regressed. A constant set to a deliberate economic value is not a finding; an arithmetic or ordering error in how it is used is.\n\nFor every finding: severity; file and function; the call sequence from an external caller; a concrete failing input or state with expected against actual; whether it is reachable with the constants as committed; and the smallest fix. Also report every place a comment or NatSpec claims a property the code does not have, and say which contracts you read in full and which you could not reach.","parentJobId":null,"planHash":"25161f1661a52208b19e6b3f689b484aa7916d43baa78589e3e1d2145c90e8e1","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"08a12413-5e3b-4baf-9f1c-6d1ba99e9487","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52165","feedbackHash":"1bdd4d77e23953a46d29b5beeee46a35fde99637eb37c22b588007d18ee4c7ec","nodeKey":"audit_economics","submissionHash":"e05d723760896697b6c4a5b37de15eb822acb440e4de36708791f44ec42c4158","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52169","feedbackHash":"7505b55b4a4e074d7ea2f438c1895d94cf09e49a812e8422244d9727978642a9","nodeKey":"audit_flow","submissionHash":"d41577c5b95f9f7d0edc2599daa6dc5f0f6e549de4708c27237e12749591cc82","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51367","feedbackHash":"298c77f09793bc2eba3efea833268457fbbf234f1f6f75d4e7d8dab15612ec86","nodeKey":"audit_judge","submissionHash":"7758891ff479ab74fb08620c8e41a607dcea8c323597716b0f57a7113c48206b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52173","feedbackHash":"473babf3c778c9f377f69daa41f4cb060231fc832b151ccf394ed96a4fc6c811","nodeKey":"audit_math","submissionHash":"8001dd351874f80a5f54e3231903526de4eb9496645c86933f4d1a67979e2a3d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52161","feedbackHash":"bb18317b86514539a3f54996f9ae1acd2f84a45a860a7a46102202438e4636b1","nodeKey":"audit_permissions","submissionHash":"3b898475802ac7311dfa249a472f2d742d653dd6a385d9a090ecf54f76416ba8","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"22ae619ba99f1d48d16f824e76624531a858992b1c39196b8cff099714015fc0","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ca075d17c94a854b","findings":[{"citation":"resolved","description":"Q1(c) and Q4 (the first hours after launch). The final vault panel's low #6 was answered by flooring the fee base at 1,000 imdUSD (_feeBaseFloor), and the resolution says 'a dust redemption while most supply is new cannot store the cap for everyone'. That is true of dust only. The stored rate is still _redemptionRate(amount, prior) with prior = max(warm base, 1_000e18), and cash stores it unchanged whenever the burn has no fresh part, which every reserve-funded burn lacks (principalCancelled = 0, so freshCancelled = 0). So while the warm supply is under 1,000 imdUSD (the launch window: a 100,000 draw is 38.5 warm after 12 s, about 1,500 after an hour, so the window lasts about an hour per large draw and reopens whenever the warm supply turns over) any burn of at least 90 imdUSD measures as 90 / 1000 / 2 = 4.5% and stores the cap. The pinner pays the 5% fee on 90 (4.5 imdUSD) and opens the reserve route by sending ~100 IMD-worth of collateral to the Treasury (redemptionReserve is gem.balanceOf(treasury), listed or not; the first liquidation cut opens it too), which the redemption pays back less the fee. Everyone then pays 5% to redeem (peg floor min(1 - fee, backing) is 0.95 instead of 0.995), 2.76% twelve hours later, 1.63% a day later; the honest stored increase for 90 of a 100,000 supply is 4.5 bps (quote 55 bps), and pinning the cap honestly on a warm 100,000 supply costs about 9,000 redeemed at ~5% = 450 imdUSD, a hundred times more. Reachable with the constants as committed (floor 1,000, divisor 2, LINE $1M, wage 0). Not an extraction: a griefing of the peg band and of every redeemer for about two days per pin, for ~4.5 imdUSD, repeatable while the window lasts; who gains is a candidate borrower deterring redemptions against itself or anyone short the peg. Smallest fix (fits the 2,127-byte runtime margin): keep charging the redeemer against prior, but bound the STORED increase by the live supply as well, e.g. in cash compute the stored rate from max(prior, stablecoin.totalSupply() - _principalMintedThisTransaction()) when freshCancelled == 0; or raise _feeBaseFloor to a figure the launch book makes irrelevant and say so in docs/MAINNET-RUNBOOK.md section 7 (open redemptions a day after the first draws).","line":1052,"path":"src/CDPVault.sol","reproduction":"test/scratch/FeeFloorPin.t.sol (fails on this code). ParameterizedVault over MockIMD at $1 (Chainlink 2000e8 etched at CHAINLINK_ETH_USD, TreasuryFactory etched at TREASURY_FACTORY), NHI 0.85, launch constants. BORROWER lock(400_000e18), draw(100_000e18), hands PINNER 90 imdUSD; 12 seconds pass; PINNER transfers 100 IMD to vault.treasury() and calls cash(90e18, 0, address(0)). EXPECTED: redemptionBaseRate about 4.5e14 (90 / 100,000 / 2) and redemptionFeeBps(0) about 55. ACTUAL: quoted fee 500 bps, 85.5 IMD paid out, redemptionBaseRate == 45000000000000000 (the cap), redemptionFeeBps(0) == 500 now, redemptionFeeBps(1e18) == 276 at +12 h and 163 at +24 h.","severity":"low","snippet":"        return base > floor ? base : floor;","title":"CDPVault._feeBase: the 1,000 imdUSD floor stops a dust pin but a 90 imdUSD reserve-funded burn still stores the 4.5% cap for everyone while most supply is new (cost 4.5 imdUSD, honest cost ~450)"},{"citation":"resolved","description":"Q5 (what can happen between the two stages). The split into run() (everything but the vault) and runVault() answers the sweep panel's oracle low: 'with the vault absent until the first values are checked, a raced first value can price nothing'. The vault is not absent at anyone's discretion but the deployer's: its initcode is the committed source plus six public constructor arguments (_vaultInit), its salt is a source constant (SALT_VAULT), and the canonical deterministic deployer at 0x4e59b44847b379578588920cA78FbF26c0B4956C forwards any caller's salt and initcode with no sender check. Every constructor dependency is on chain the moment stage one lands (WORK_ORACLE_FACTORY, TREASURY_FACTORY, the three feeds, sIMD), so after run() a stranger can create the vault at exactly the planned address (the scratch test does it from address(0xBAD) in 13.8M gas). Two consequences. (a) The stated property fails: the stranger relays a first value off a pumped pool (nothing bounds a first value), deploys the vault, locks sIMD and draws in the same block, before verifySeeded ever runs; the imdUSD is worthless unless the operator adopts the vault, so the economic loss is nil, but it is the sequence the two-stage deploy claims to make impossible. (b) Launch liveness: runVault's _deploy prints 'exists, skipped' for an address that holds code and verify() then fails on vault.stablecoin().totalSupply() == 0 (or totalDebt, pendingEta, reserveAssetCount), so a griefer who deploys the planned vault and draws one wei against honest seeded feeds (3 attestations, 1.5 IMD, plus a small lock) forces a new salt and a new rehearsal every time, for about 14M gas (~0.025 ETH at the 1.7 gwei the script itself waits for), and can repeat against each new salt since the salts are 'part of the deploy commit'. A rogue vault that is deployed but never used passes verify and is harmless. Reachable with the script as committed on mainnet. Smallest fix: make the vault's creation sender-bound, e.g. deploy it with plain CREATE from the throwaway deployer (its address is not a source constant, so nothing needs it to be CREATE2) and have verify() read it from the broadcast; or keep CREATE2 but salt it with the deployer address (a tiny factory that checks msg.sender, or a salt kept out of the repository until stage two); and reword the comment at 178-184 and docs/MAINNET-RUNBOOK.md section 7 ('a raced first value prices nothing') to what the stage split actually buys, which is a check the operator runs before THEIR deploy.","line":183,"path":"script/DeployMainnet.s.sol","reproduction":"test/scratch/VaultFrontRun.t.sol (passes, demonstrating the sequence). Etch SwarmRelay at ATTESTATION_RELAYER, WorkOracleFactory at WORK_ORACLE_FACTORY, TreasuryFactory at TREASURY_FACTORY; deploy PriceFeed, NhiFeed, SpotFeed with (maxAge, 2000) unseeded; initcode = type(ParameterizedVault).creationCode ++ abi.encode(gem, 0, WORK_ORACLE_SENTINEL, price, nhi, spot); planned = vm.computeCreate2Address(keccak256('infer-protocol/mainnet/v1/ParameterizedVault'), keccak256(initcode), 0x4e59b4...956C). From address(0xBAD): DEPLOYER.call(salt ++ initcode). EXPECTED (the script's premise): only the operator's runVault, after verifySeeded, can create the vault. ACTUAL: the call succeeds, returns the planned address, planned.code.length > 0, vault.treasury().vault() == planned, and DeployMainnet._deploy would report 'exists, skipped'.","severity":"low","snippet":"    /// checked, a raced first value can price nothing; it costs the operator a wait for the allowance to","title":"DeployMainnet two-stage deploy: anyone can place the planned ParameterizedVault at its CREATE2 address between run() and runVault(), so the vault can be live before verifySeeded and any planned salt c"},{"citation":"resolved","description":"CLAIMS WITHOUT THE PROPERTY. (1) DeploymentConfig 36-37, 'Treasury-funded staleness asks apply to [NHI] alone': OracleAsker.ask pays for ANY feed that is wideOpen (silent a lifetime and allowance >= WIDE_ALLOWANCE_BPS), price and spot included, as DeploymentConfig 241-249 and PARAMETERS-2026-10-05 say; the two comments in the same file contradict each other and the first is the wrong one. (2) DeployMainnet 178-184 and docs/MAINNET-RUNBOOK.md 321-324, 'with the vault absent until the first values are checked, a raced first value can price nothing': anyone can create the planned vault after stage one (low finding above). (3) CDPVault 1043-1046 and the panel resolution ('a dust redemption ... cannot store the cap for everyone'): true of dust; 90 imdUSD still does (low finding above). (4) CDPVault 665, cash's @notice 'for feed-priced IMD, less the capped fee': the payout is also scaled by backingPerUnit below par (the @dev at 683-711 says so; the @notice does not). (5) CDPVault 183-184, 'raises the fee base by redeemed / supply / this': the divisor is applied to redeemed / _feeBase (warm supply plus warm repayments, floored), not the live supply. (6) ParameterizedVault 155, 'All idle IMD is usable': redemptionReserve is the Treasury's balance of the COLLATERAL (sIMD on mainnet); plain IMD the Treasury holds is never paid to a redeemer (it is spent by fundOracle). (7) docs/MAINNET-RUNBOOK.md 345-346, 'Treasury.fundOracle pays only from sIMD the Treasury holds, and the Treasury's only sIMD income is the liquidation cut', contradicted four lines later by 'fundOracle spends the Treasury's plain IMD first and unwraps sIMD after it', which is what Treasury.fundOracle 531-535 does; the first sentence is stale. (8) CDPVault 1324-1326, tail() 'the shorter feed lifetime': it is min(priceFeed.maxAge, nhiFeed.maxAge) and ignores the spot feed's and Chainlink's lifetimes; at the committed constants the answer (1 hour) is the same, so this is an imprecision, not a defect. (9) CDPVault 759-762, the premium's bound '15% of [principal] at a 170% minimum ratio': the stated general bound (principal above half the collateral's value) holds, but the premium exists only below par, which is exactly when positions sit below mat (wipe has no health check), so the operative figure is up to half the principal at 100% and more below it; the general bound and the formula (supply - fresh) / (supply - fresh - repaid) stay correct. ANSWERS WHERE NOTHING IS WRONG. Q1(a): nothing of 58f73de remains (grep 'excess' in src/ finds only feeExcess, _feeExcess, _moveFeeExcess; Position has no excess field; free, draw and wipe carry no _moveExcess). REPAID_THIS_TX_SLOT is added in _payDebt (wipe, bite) and in cover, never in _redeemPosition, and _backingPerUnit reads supply = totalSupply + repaid for both the live and the lagged figure, so a same-call wipe W, cash, draw W measures the burn against the pre-wipe supply while the numerator can only fall (the mat cap term falls by 1.7 W when it binds; a collateral-bound term does not move), which underpays, never overpays; the fee remint in _payDebt over-counts the supply by feePaid in the same call, also the safe direction. Cross-transaction the premium is at most (supply - fresh) / (supply - fresh - repaid) with repaid <= principal - collateral x price / 2, as stated; it needs backing below par, i.e. the sum of secured terms at price below the supply (aggregate collateral under the debt), and a redraw needs the churner healthy again. Q1(b): _coldAt is written only in _lag, in the same statement as the touched position's coldAt, so every position's elapsed >= the total's elapsed; cooling is monotone in elapsed, the total rounds up and the positions down, and the cutoff applies to both, so the total is at least the sum of the positions' cold up to _pow's last-digit rounding (about 1e-26 relative, dust at sIMD's 24 decimals) and a decrease (coldOut = min(cold, out)) never removes more than its position put there: no sequence w","line":37,"path":"src/DeploymentConfig.sol","reproduction":"(1) read OracleAsker.ask line 162: forStaleness = (f.keepAlive && nearStale(feed)) || wideOpen(feed), with wideOpen true for a price feed silent ten hours. (2) test/scratch/VaultFrontRun.t.sol. (3) test/scratch/FeeFloorPin.t.sol. (4)-(6) read the cited lines against cash 711-712, _redemptionRate 891 and ParameterizedVault.redemptionReserve 157. (7) read Treasury.fundOracle 531-535. (8) read tail() 1328. (9) read _backingPerUnit and wipe (no health check).","severity":"info","snippet":"/// Treasury-funded staleness asks apply to it alone. `tail()` is the shorter of PRICE and NHI.","title":"Comments and NatSpec that claim properties the code does not have at 6085c8a; the answers to Q1-Q6 where nothing is wrong; coverage"}],"hash":"3b898475802ac7311dfa249a472f2d742d653dd6a385d9a090ecf54f76416ba8","nodeId":"5b704c26-f8ce-4641-8e4c-4485a1ceee91","outcome":"completed","summary":"The committed suite is green (599 passed, 4 skipped), the findings file validates, and every snippet matches its cited line. The review is complete.\n\n**Outcome.** Three entries are in `.imd-findings.json`: two lows with reproductions and one info entry carrying the comment-claim list, the answers to the six questions where nothing is wrong, and the coverage statement. No critical, high or medium defect survived verification.\n\n**Low, fee base floor** (`src/CDPVault.sol:1052`). The floor stops a dust pin, as the resolution says, but the stored rate is still measured against the floored base. While the warm supply is under 1,000 imdUSD, any reserve-funded burn of 90 imdUSD stores the 4.5% cap for everyone. The scratch test shows a 100,000 draw, then 90 burned twelve seconds later: quoted 500 bps, stored rate 0.045e18, still 276 bps twelve hours on. The pin costs 4.5 imdUSD where the honest route costs about 450. Smallest fix is to bound the stored increase by the live supply as well as the warm base.\n\n**Low, stage gap in the deploy** (`script/DeployMainnet.s.sol:183`). The two-stage split claims the vault is absent until the first values are checked. Its initcode and salt are public and the canonical CREATE2 deployer has no sender check, so after stage one a stranger can create the planned vault at its planned address. The scratch test does this from an arbitrary address in 13.8M gas. A rogue vault with a drawn position fails the operator's verify step, so no funds are at risk, but the stated property does not hold and each planned salt can be burned cheaply. Fix: create the vault with plain CREATE from the deployer, or bind the salt to the sender.\n\n**Where nothing is wrong.** The excess of 58f73de is gone entirely. The same-transaction repayment tally closes the wipe, cash, draw churn and can only underpay. The uniform quiet-day cutoff holds because the total's clock is never older than any position's, and a stale position reading zero only understates. The accepted premium's bound is correct as stated. No cross-subsystem sequence I traced mints unbacked imdUSD, blocks liquidation or redemption, or desynchronises a record. The cheapest profitable oracle manipulation remains the documented multi-hour walk, at roughly $20k of fees plus about $1M of ETH parked in the pool for three to four hours, for about $150k to $180k at the $1M line.\n\n**Comment claims that do not hold.** Nine are listed in the info entry. The two that matter: the DeploymentConfig line saying Treasury staleness asks apply to NHI alone, when `ask` also pays for any feed that is wide open, and the runbook sentence that the Treasury funds the oracle only from sIMD, contradicted four lines later and by the code.\n\n**Scratch files.** `test/scratch/FeeFloorPin.t.sol` fails on this code as the finding states. `test/scratch/VaultFrontRun.t.sol` passes and demonstrates the sequence. Both are disposable.","treeHash":null,"usage":{"cachedInputTokens":3964450,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":90838,"runtime":"claude","turns":36,"wallClockMs":1481562}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ddfb1efa72fe9a94","findings":[{"citation":"resolved","description":"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.","line":1057,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// Whole-system audit at 6085c8a, Q1(c): the fee-base floor (CDPVault._feeBaseFloor, 1,000 imdUSD) closes the\n// launch pin for DUST only. At divisor 2 any reserve-funded burn of 90 imdUSD or more against the floored base\n// (90 / 1,000 / 2 = 4.5%) still STORES the cap as redemptionBaseRate for everyone, for 4.05 imdUSD of fee.\n// Fails on 6085c8a: redemptionBaseRate() == 0.045e18 after the burn and the 1 imdUSD quote 18 hours later\n// reads 210 bps where the honest increase is under a basis point.\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract FpFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 days;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external pure returns (bool) {\n        return false;\n    }\n}\n\ncontract FpMirror is ISwarmFeed {\n    ISwarmFeed private immutable primary;\n\n    constructor(ISwarmFeed p) {\n        primary = p;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return primary.latestValue();\n    }\n\n    function isStale() external view returns (bool) {\n        return primary.isStale();\n    }\n\n    function maxAge() external view returns (uint256) {\n        return primary.maxAge();\n    }\n}\n\ncontract FpAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract ProofFloorPinTest is Test {\n    address private constant BORROWER = address(0xB0B);\n    address private constant PINNER = address(0x919);\n\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    ImdUSD private stable;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new FpAggregator()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        FpFeed primary = new FpFeed(uint256(1 ether) * 1e18 / 2000 ether); // IMD = $1\n        FpFeed health = new FpFeed(0.85 ether); // mat 170\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(health), address(new FpMirror(primary))\n        );\n        stable = vault.stablecoin();\n        address treasury = address(vault.treasury());\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(BORROWER, 400_000 ether);\n        imd.mint(treasury, 1_000 ether); // the burn is reserve-funded: no fresh part, the quote is stored unchanged\n        vm.stopPrank();\n    }\n\n    function test_ninetyImdUsdReserveFundedBurnStoresTheCapForEveryone() public {\n        // Launch: the first loan is all the supply there is, and all of it is cold.\n        vm.startPrank(BORROWER);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(400_000 ether);\n        vault.draw(100_000 ether);\n        stable.transfer(PINNER, 90 ether);\n        vm.stopPrank();\n        vm.warp(block.timestamp + 12);\n        // Warm supply is about 38.5 imdUSD, so the base is the 1,000 floor: 90 / 1,000 / 2 is the whole cap.\n        vm.prank(PINNER);\n        vault.cash(90 ether, 0, address(0));\n        uint256 cap = (vault.REDEMPTION_FEE_CAP_BPS() - vault.REDEMPTION_FEE_FLOOR_BPS()) * 1e14;\n        assertLt(vault.redemptionBaseRate(), cap, \"a 90 imdUSD burn must not store the cap as everyone's base rate\");\n        // Eighteen hours later 7/8 of the supply is warm and the honest increase for 1 imdUSD is under a basis\n        // point, so an honest quote is the 50 bps floor plus at most a few bps.\n        vm.warp(block.timestamp + 18 hours);\n        assertLt(vault.redemptionFeeBps(1 ether), 100, \"the stored pin must not still price a 1 imdUSD burn above 1%\");\n    }\n}","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).","severity":"low","snippet":"        return 1_000e18;","title":"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"},{"citation":"resolved","description":"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.","line":212,"path":"script/DeployMainnet.s.sol","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.","severity":"low","snippet":"        _deploy(SALT_VAULT, _vaultInit(p), p.vault);","title":"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"},{"citation":"resolved","description":"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.","line":775,"path":"src/CDPVault.sol","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.","severity":"low","snippet":"            uint256 lagged = Math.mulDiv(reserve + _securedCollateralValue(price, true), 1e18, supply - fresh);","title":"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"},{"citation":"resolved","description":"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.","line":1019,"path":"src/CDPVault.sol","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.","severity":"info","snippet":"    /// until it cools. That only understates the lagged figures, the safe direction: accepted (retry2, low).","title":"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"},{"citation":"resolved","description":"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","line":753,"path":"src/CDPVault.sol","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.","severity":"info","snippet":"    /// figure but not the lagged one, whichever position it sits in (`_lag`); honest redemptions are not","title":"Comments and NatSpec that claim properties the code does not have at 6085c8a; the answers to Q1-Q6 where nothing is wrong; coverage"}],"hash":"7758891ff479ab74fb08620c8e41a607dcea8c323597716b0f57a7113c48206b","nodeId":"82f973a8-4b48-494d-ab9f-beeeabb4d941","outcome":"completed","summary":"The review is complete. The findings file holds five entries, every snippet checked against the tree, and the baseline suite is green on the pinned commit (599 passed, 0 failed, 4 skipped).\n\n**What survived, merged and reproduced**\n\n- **Low, fee floor (CDPVault 1057).** Three specialists reported the same thing and all three reproduce: the 1,000 imdUSD floor stops a dust pin but a 90 imdUSD reserve-funded burn still stores the 4.5% cap for everyone. The attached proof fails on this code; the stored rate reads the cap at once and the 1 imdUSD quote still reads 210 bps 18 hours later and 79 bps at 48 hours. Panel #6's \"Fixed\" is partial. The proof passes under either suggested fix (bound the stored increase by the live supply, or raise the floor).\n- **Low, CREATE2 front-run between the stages (DeployMainnet 212).** Four specialists reported it. My scratch test lands the vault at the planned address from a stranger through the canonical proxy in 13.8M gas. The stated property (\"a raced first value can price nothing\") is unenforced. Rated low rather than the one medium vote because the operator detects it in verifySeeded or verify and the cost is a re-salt, not funds. Fix: deploy the vault with plain CREATE in stage two, or a sender-bound salt.\n- **Low, reserve credited against the warm supply alone (CDPVault 775).** One specialist, reproduced: in the launch window the lagged figure is vacuous and a newcomer's one-transaction-old capital lifts a reserve-funded redemption to par over a book backed at 0.95. Reads 1.0 at 12 seconds, 1 hour and 6 hours of supply age, honest at 24 hours. Bounded by reserve times the gap below par; needs Treasury sIMD and a below-par book in the first hours. Fix is partly operational, so no proof attached.\n- **Info, orphan direction (CDPVault 1019).** The \"only understates\" claim is exact for laggedNow but a debt-side orphan raises the backing cap's lagged figure by a bounded amount (0.065% in the reproduction).\n- **Info, comments list plus the Q1 to Q6 answers and coverage (CDPVault 753).** Sixteen comment or doc claims verified by reading, including the stale Treasury.fundOracle revert claim, the stale \"NHI alone\" sentence in DeploymentConfig, the three \"redeemed / supply\" divisor comments, the runbook's 10% drift figure and its self-contradiction on fundOracle, and the premium parenthetical whose below-par magnitude is 25% in my reproduction.\n\n**What I dropped or folded.** Nothing was dropped as false. The separate infos on the Treasury NatSpec, the divisor NatSpec, the runbook drift figure and the premium magnitude are folded into the comments entry rather than kept as duplicates. The two accepted items stand: the premium's bound formula is correct and the feeExcess early release is bounded as stated.\n\n**Not findings, answered in the info entry.** Q1(a) nothing of 58f73de remains and the same-call tally closes wipe, cash, draw. Q1(b) no sequence warms one position's capital with another's. Q2 no cross-subsystem sequence mints unbacked imdUSD. Q3 the cheapest profitable over-borrow is roughly three hours holding IMD's pool at 2x with about $1M of ETH and $20k of fees for at most about $150k, visible throughout. Q4 every halt recovers through a relayed attestation or Chainlink answering. Q5 nothing else found that deploys wrong and passes.\n\nScratch tests are under test/scratch/ and are disposable; no tracked file was changed.","treeHash":null,"usage":{"cachedInputTokens":3896737,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":53252,"runtime":"claude","turns":42,"wallClockMs":913610}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c2e167266d13dcf4","findings":[{"citation":"resolved","description":"Q5. `_deploy` sends `salt ++ initcode` to CREATE2_FACTORY (0x4e59b44847b379578588920cA78FbF26c0B4956C, Arachnid's deterministic deployment proxy), whose address is a pure function of (salt, initcode) with no dependence on the caller. SALT_VAULT is a literal in the script and `_vaultInit` is `type(ParameterizedVault).creationCode ++ abi.encode(STAKED_IMD, 0, WORK_ORACLE_SENTINEL, price, nhi, spot)`, all public once the deploy commit is published (and reproducible from the verified feed bytecode and DeploymentConfig). The two-stage split (run, seed + verifySeeded, runVault) exists so that a pumped first value relayed by a stranger 'can price nothing' because no vault exists yet. But as soon as stage one has put TREASURY_FACTORY and WORK_ORACLE_FACTORY on chain, any account can call the proxy with the same salt and initcode: the constructor needs nothing from the caller, so the vault lands at the planned address, creates its imdUSD, Parameters, Treasury and work oracle, and is live. In one transaction a stranger can relayMany pumped first values to the price and spot feeds (nothing on chain bounds a first value), deploy the vault, lock sIMD and draw up to LINE against the pumped price, all before the operator's verifySeeded is ever run. The operator's `runVault` then prints 'exists, skipped' and `verify` reverts on `totalSupply() == 0` / `totalDebt() == 0`, so the compromised vault is detected, but the stated property ('never a redeploy') is false: the operator must pick a new salt, and since every salt is in the committed script the same race applies to the next one. If the stranger instead seeds honest values and deploys without drawing, verify passes and the vault is simply live earlier than planned (harmless). Reachable with the constants as committed; cost to the attacker is the pump (round-trip pool fees) plus ~13.8M gas; profit is bounded by the operator abandoning the vault, so this is griefing that forces a redeploy, or real bad debt only if the operator proceeds past a failed verify. Smallest fix: in stage two deploy the vault with CREATE from the throwaway deployer (`new ParameterizedVault(...)` inside the broadcast) rather than through the public proxy, or use a proxy that mixes msg.sender into the salt (ImmutableCreate2Factory-style, or prefix the salt with the deployer address and check it); and reword the NatSpec at 178-184 and docs/MAINNET-RUNBOOK.md 321-324.","line":183,"path":"script/DeployMainnet.s.sol","reproduction":"test/scratch/Probe2.t.sol (passes, demonstrating the deployment): etch the canonical proxy runtime at CREATE2_FACTORY, TreasuryFactory at TREASURY_FACTORY and WorkOracleFactory at WORK_ORACLE_FACTORY (stage one), deploy PriceFeed/NhiFeed/SpotFeed unseeded, compute `planned = vm.computeCreate2Address(SALT_VAULT, keccak256(init), proxy)` with the script's SALT_VAULT and `_vaultInit` encoding, then from address(0xA77) call `proxy.call(bytes.concat(SALT_VAULT, init))`. EXPECTED (per the NatSpec): no vault can exist before runVault. ACTUAL: the call returns 0x1d8fB095BBf84cDDCACabf9Bf5dE8F9Be484DdEf == planned, 22,449 bytes of code, `ParameterizedVault(planned).treasury().vault() == planned`; `_deploy` in runVault would skip it. Gas 13,792,063.","severity":"low","snippet":"    /// checked, a raced first value can price nothing; it costs the operator a wait for the allowance to\n    /// widen and honest values, never a redeploy.","title":"DeployMainnet: between stage one and stage two anyone can deploy ParameterizedVault at its planned CREATE2 address through the permissionless deterministic-deployment proxy, so 'the vault is absent un"},{"citation":"resolved","description":"Q1(b). The claim holds for laggedNow(): a position untouched for BACKING_WARMUP reads zero while the total, touched 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)) / (supply - fresh)` with `fresh = totalDebt - lagDebt`, so an orphan on the DEBT side 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). 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/16...` instead of `supply`. The orphan is at most the position's day-old cold debt times 2^-4 and it 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 P_A / 16) / supply, well below 1% and far below the accepted repay-then-redeem premium. Not exploitable for profit above the redemption fee; reported because the task asks whether the remainder 'only understates' and the resolution's 'safe direction' is not exact for the cap. Fix, if wanted: none needed for safety; or cool the totals without the cutoff as the panel proposed, which the resolution rejected for consistency.","line":1019,"path":"src/CDPVault.sol","reproduction":"test/scratch/Probe1.t.sol test_orphanDirection (logs). ParameterizedVault over MockIMD at $1 (Chainlink 2000e8), 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() 0.9545e18, laggedNow() == (11,000e18, 21,000e18+1). A draws 150 (debt-only: term stays 4,000). T (lock 100) draws 1 every 6 hours for a day to keep the total busy; +1 s: A reads zero cold, coldPrincipal() (total) == 11.25e18 (A's 150/16 plus T's). backingPerUnit() == 942083557468172852 (the live figure, 10,508/11,154 = A fully warm, the honest value). U then locks 2,000 (cold secured only): EXPECTED the lagged minimum to stay at or below 0.942083e18 (U's cold capital excluded, A warm). ACTUAL backingPerUnit() == 942698147227049462: the binding figure is now the lagged one, computed over supply - 11.25 instead of supply, 0.065% above the honest value.","severity":"info","snippet":"    /// until it cools. That only understates the lagged figures, the safe direction: accepted (retry2, low).","title":"CDPVault._cool: the accepted 'stale position reads zero while a busy total keeps its share' gap understates laggedNow() as stated, but understating lagDebt alone RAISES the lagged backing figure in _b"},{"citation":"resolved","description":"Q1(a), the accepted item. The bound formula ((supply - fresh) / (supply - fresh - repaid), repaid at most the principal above half the collateral's value) is correct as written, so the acceptance's reason is not wrong; what the parenthetical '15%' understates is the magnitude where it applies. The premium exists only below par, which is exactly when borrowers are at or under 100% CR, and a borrower at CR r can repay W = P(1 - r/2) without moving its collateral-bound term (W = 0.5P at r = 1.0, 0.6P at r = 0.8). With a dominant churner (P close to supply) the lagged and live figures both rise by supply / (supply - W), i.e. 1.43x at r = 0.8 in the reproduction, capped at par. The redeemer (any holder, or the churner through a second key) is then paid at par against the Treasury's sIMD or any candidate whose CR is at least the payout per unit, out of the backing of everyone who did not redeem. Cost to the churner is its own repayment (a par trade for debt relief it would make anyway in a crash) and gas; no seasoned capital, repeatable each time it has imdUSD to repay. Reported as information for the accepted item: the number the resolution states (15%) should be the crash-regime bound (up to 50-60% of the churner's principal, the payout capped at par).","line":762,"path":"src/CDPVault.sol","reproduction":"test/scratch/Probe1.t.sol test_premiumUnderwater (logs). ParameterizedVault over MockIMD at $1 (Chainlink 2000e8), NHI 0.85, Treasury empty. X (churner) locks 10,000 and draws 5,000; U locks 10,000, draws 5,000 and hands R 5,000 imdUSD; two days; IMD to $0.50 then $0.40, both re-priced by lock(1), two quiet days each: backingPerUnit() == 0.8e18 (both at 80%). Honest (snapshot): R cash(1,900e18, 0, X) pays 3610e18 raw IMD. Churn: X wipe(3,000e18) (W = 5,000 - 10,000 x 0.4 / 2; its term stays 10,000): backingPerUnit() == 1e18 (capped). R cash(1,900e18, 0, X) pays 4512.5e18 raw IMD, +25%. EXPECTED by the stated '15%' bound: a premium of at most 1 / (1 - 0.15 x 5,000 / 10,000) = 8%.","severity":"info","snippet":"    /// repayment bounded by the churner's principal above half its collateral's value (15% of it at a 170%","title":"CDPVault._backingPerUnit: the accepted repay-then-redeem premium's '15% of its principal at a 170% minimum ratio' describes a healthy churner; below par (the only regime where the premium exists) `wip"}],"hash":"8001dd351874f80a5f54e3231903526de4eb9496645c86933f4d1a67979e2a3d","nodeId":"47ec53cd-f6c8-4b0f-9e48-6b6872dfaff4","outcome":"completed","summary":"Findings are written to `.imd-findings.json` (1 low, 2 info; all snippets verified against the tree). Scratch probes are under `test/scratch/` (Harness, Probe1–3). Baseline suite: 599 passed, 0 failed, 4 skipped (fork tests).\n\n## Answers\n\n**1(a) Excess removal and the repaid tally.** No remnant of 58f73de remains: `Position.excess`, `_excess`, `_excessNow`, `_moveExcess` and the moves in `wipe`/`free`/`draw` are gone (grep on `excess` only hits `feeExcess` and error names). `REPAID_THIS_TX_SLOT` is added in `_payDebt` (wipe, bite) and in `cover` after its burn, not in `cash`; `_backingPerUnit` adds it to the supply of both the live and lagged figure. Probe3: a one-transaction lock/draw/wipe/cash/draw churn pays 1,017 IMD where the honest redemption pays 1,900 — closed, in the conservative direction. The accepted cross-transaction premium's bound formula is correct; the parenthetical \"15%\" is the healthy-churner figure only. Below par (the only regime where the premium exists) `wipe` has no health check, so a churner at CR 100% repays 50% of its principal without moving its term and the payout hits the par cap (+25% in Probe1). Reported as info on the accepted item, with numbers.\n\n**1(b) Uniform cutoff.** Proven consistent: `_lag` is the only writer of `_coldAt` and `position.coldAt`, and it writes both on every real change, so the total's elapsed ≤ every position's elapsed; a zeroed total implies zeroed positions, and with ceil (totals) vs floor (positions) rounding, `coldOut ≤` the position's share — no position's decrease can retire another's cold. The remainder (stale position reads zero, busy total keeps ≤1/16 of its cold) does understate `laggedNow()`, but understating *lagDebt* alone shrinks `supply − fresh` and *raises* the lagged backing figure; Probe1 shows backingPerUnit moving from 0.942083 to 0.942698 when a cold lock makes the lagged figure binding. Bounded by (band draw/16)/supply — well under 1%. Reported as info.\n\n**1(c) Fee-base floor.** Cannot dilute once past it: the floor is `max(base, 1000e18)`, so it is inert whenever warm supply > 1,000 imdUSD, and it only ever lowers fees for redemptions under ~90 imdUSD (above that the 4.5% cap binds regardless). Cold and work-minted supply are excluded, repaid warm principal is added, redemption burns and cold repayments net to zero on the base (checked algebraically for wipe/draw/redeem/redraw). The single pre-state read is the figure the retry panel asked for; the stored rate uses it for both the charge and the stored rate. No finding.\n\n**2. Cross-subsystem sequences.** Checked wipe→cash→draw, bite→cash, cover→cash, cash→wipe→cash, same-position wipe→draw and cross-position wipe/draw for the work ceiling (`_debtAtTransactionStart` + per-position banks), free→lock (bankSecured), price-rise→draw (bank credits a term the collateral always carried), redeem→redraw (accepted low; the release can only raise the fee). Donations to the Treasury raise backing but cost the donor more than any redeemer gains. Nothing extracts value or desynchronises an accounting record; the only new sequence is the stage-two deployment race (finding 1).\n\n**3. Oracle as attack surface (LINE $1M, pool ~$2.3M/side, 1% fee).** Moving the pool by factor k costs ≈ 2·R·(√k−1)·1% in round-trip fees and must be held for ≥7 of the primary's 13 samples (≥~1 h of a 2 h window) plus the spot's last block. Fresh epoch (≤20%): ~$4.4k, not profitable — the loan sits at 141% true CR and is liquidated when the feed follows. Over-borrowing pays only above k = mat (1.7): allowance 70% after 14 h of silence past the lifetime (cost ≈ $15k fees, profit ≈ $29k at k=1.75, 17 h silence), 100% after 26 h ($19k fees, ~$150k profit) — all require nobody to buy a 0.5 IMD refresh for 14–26 h while `wideOpen` (10 h) is triggerable by anyone and the keeper fallback exists. Liquidation timing: sell ≈$270k IMD (−20%), ≈$5.4k fees, hold ~8 h (window + 6 h lull + tail) and bite positions in the 170–212% band for ~+45% of debt at ","treeHash":null,"usage":{"cachedInputTokens":13072255,"inputTokens":104,"model":"claude-fable-5-1","outputTokens":94666,"runtime":"claude","turns":54,"wallClockMs":1827697}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e8816d4386532a66","findings":[{"citation":"resolved","description":"Q1(c). _feeBase() returns max(warm base, 1,000e18). _redemptionRate() then 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 cap, and cash() stores `base` unchanged whenever the burn has no fresh part (line 727), which every reserve-funded burn lacks. So while most supply is cold (the first hours after launch, or after any large honest draw) a 90 imdUSD redemption pinned through the Treasury's sIMD (redemptionReserve is gem.balanceOf(treasury): the runbook seeds it, the first liquidation cut lands there, and anyone can donate 6 sIMD to open the route) stores 0.045e18 as redemptionBaseRate for everyone, for 4.05 imdUSD of fee, where the honest increase for 90 imdUSD against the live 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: 18 hours later 7/8 of the supply is warm, the honest increase for 1 imdUSD is under a basis point, and the quote reads 210 bps; 163 at 24 h, 79 at 48 h. These are exactly the numbers of final-vault-panel finding #6, whose resolution 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. Reachable with the constants as committed, at launch (LINE $1M, divisor 2, wage 0), by anyone holding 90 imdUSD; repeatable each time the warm supply turns over. Not an extraction: the pinner pays the cap on its own burn; the harm is the throttle (the rate everyone after pays, the cost that slows a redemption run and makes the b952037a self-redemption pump expensive) being set to its maximum on demand for ~$4 while the protocol is young, so the peg floor min(1 - fee, backing) sits at 0.955-0.98 for two days instead of 0.995. Answer to the rest of Q1(c): once the warm base is past the floor the floor is dead (max of the two) and cannot dilute anything, and the single pre-touch read of `prior` makes the quote and the stored rate agree (the only remaining imprecision is that a burn cancelling COLD-but-not-fresh principal is charged in full against the warm base, the higher-fee direction). Smallest fix: bound the STORED increase by the burn's share of the live supply while still charging the redeemer against `prior`: in cash(), `redemptionBaseRate = min(freshCancelled == 0 ? base : _redemptionRate(amount - freshCancelled, prior), decayedRedemptionBaseRate() + mulDiv(amount, 1e18, stablecoin.totalSupply()) / redemptionDivisor())` (two lines, fits the 2,490-byte initcode margin); or raise the floor 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.","line":1057,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// Whole-system audit at 6085c8a, Q1(c): the fee-base floor (CDPVault._feeBaseFloor, 1,000 imdUSD) closes the\n// launch pin for DUST only. At divisor 2 any reserve-funded burn of 90 imdUSD or more against the floored base\n// (90 / 1,000 / 2 = 4.5%) still STORES the cap as redemptionBaseRate for everyone, for 4.05 imdUSD of fee.\n// Fails on 6085c8a: redemptionBaseRate() == 0.045e18 after the burn and the 1 imdUSD quote 18 hours later\n// reads 210 bps where the honest increase is under a basis point.\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract FpFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 days;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external pure returns (bool) {\n        return false;\n    }\n}\n\ncontract FpMirror is ISwarmFeed {\n    ISwarmFeed private immutable primary;\n\n    constructor(ISwarmFeed p) {\n        primary = p;\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return primary.latestValue();\n    }\n\n    function isStale() external view returns (bool) {\n        return primary.isStale();\n    }\n\n    function maxAge() external view returns (uint256) {\n        return primary.maxAge();\n    }\n}\n\ncontract FpAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract ProofFloorPinTest is Test {\n    address private constant BORROWER = address(0xB0B);\n    address private constant PINNER = address(0x919);\n\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    ImdUSD private stable;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new FpAggregator()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        FpFeed primary = new FpFeed(uint256(1 ether) * 1e18 / 2000 ether); // IMD = $1\n        FpFeed health = new FpFeed(0.85 ether); // mat 170\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(health), address(new FpMirror(primary))\n        );\n        stable = vault.stablecoin();\n        address treasury = address(vault.treasury());\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(BORROWER, 400_000 ether);\n        imd.mint(treasury, 1_000 ether); // the burn is reserve-funded: no fresh part, the quote is stored unchanged\n        vm.stopPrank();\n    }\n\n    function test_ninetyImdUsdReserveFundedBurnStoresTheCapForEveryone() public {\n        // Launch: the first loan is all the supply there is, and all of it is cold.\n        vm.startPrank(BORROWER);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(400_000 ether);\n        vault.draw(100_000 ether);\n        stable.transfer(PINNER, 90 ether);\n        vm.stopPrank();\n        vm.warp(block.timestamp + 12);\n        // Warm supply is about 38.5 imdUSD, so the base is the 1,000 floor: 90 / 1,000 / 2 is the whole cap.\n        vm.prank(PINNER);\n        vault.cash(90 ether, 0, address(0));\n        uint256 cap = (vault.REDEMPTION_FEE_CAP_BPS() - vault.REDEMPTION_FEE_FLOOR_BPS()) * 1e14;\n        assertLt(vault.redemptionBaseRate(), cap, \"a 90 imdUSD burn must not store the cap as everyone's base rate\");\n        // Eighteen hours later 7/8 of the supply is warm and the honest increase for 1 imdUSD is under a basis\n        // point, so an honest quote is the 50 bps floor plus at most a few bps.\n        vm.warp(block.timestamp + 18 hours);\n        assertLt(vault.redemptionFeeBps(1 ether), 100, \"the stored pin must not still price a 1 imdUSD burn above 1%\");\n    }\n}","reproduction":"test/scratch/Proof_FloorPin.t.sol (attached; fails on 6085c8a). 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 38.5 imdUSD, base = the 1,000 floor, redemptionFeeBps(90e18) == 500) PINNER cash(90e18, 0, address(0)). EXPECTED: redemptionBaseRate well under the 0.045e18 cap (the honest increase for 90 of a 100,000 supply is 4.5 bps) and redemptionFeeBps(1e18) near the 50 bps floor 18 hours later. ACTUAL: redemptionBaseRate() == 45000000000000000 (the cap) and redemptionFeeBps(1e18) == 210 at +18 h, 163 at +24 h, 79 at +48 h. Control (the committed test/final-vault-panel/FeeBaseFloor.t.sol): 5 imdUSD stores 25 bps; 90 is the threshold at divisor 2.","severity":"low","snippet":"        return 1_000e18;","title":"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 'fixed' only"},{"citation":"resolved","description":"Q5 ('what can happen between the two stages'). The two-stage deploy exists so that 'with the vault absent until the first values are checked, a raced first value can price nothing' (lines 183-184; docs/MAINNET-RUNBOOK.md section 7). Nothing enforces the absence. The vault is deployed through the canonical permissionless CREATE2 deployer (CREATE2_FACTORY, 0x4e59b4...) with a salt and initcode that are both in the repository (SALT_VAULT, _vaultInit(p): type(ParameterizedVault).creationCode ++ abi.encode(STAKED_IMD, 0, WORK_ORACLE_SENTINEL, p.price, p.nhi, p.spot)), and the constructor's only external prerequisites (TREASURY_FACTORY and WORK_ORACLE_FACTORY having code) are met the moment stage one's last transaction lands. Anyone can therefore send the same initcode and salt to the deployer and land the correct vault at p.vault before the operator seeds and checks the feeds, and `_deploy` will later print 'exists, skipped'. Sequence from an external caller after run(): (1) pump IMD's pool and buy price and spot attestations of it (the first value of a feed is bounded by nothing on chain, SwarmFeed 236-239) and relay them, plus an honest NHI; (2) CREATE2_FACTORY.call(SALT_VAULT ++ _vaultInit(p)) from any EOA (about 12.7M gas, no authority check anywhere in the constructor chain: ParameterizedVault, TreasuryFactory.create, WorkOracleFactory.create, Parameters, ImdUSD are all caller-agnostic); (3) lock sIMD and draw against the pumped price in the same block. The operator's runVault() then fails verifySeeded ('do not deploy the vault') but the vault already exists at the only address the plan has, with positions in it; verify() would also fail on totalSupply() == 0 (line 278). 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 a griefing vector that forces the operator to abandon the planned vault address and re-salt (only SALT_VAULT changes: the vault's address is not a source constant, the four prerequisites' are), or, if the attacker deploys it honestly, a verify() that passes against a vault the operator did not deploy. It also makes the claim at 178-184, the runbook's section 7 paragraph two and SwarmFeed.sol 238-239 ('before deposits open') unenforced. Reachable with the constants as committed. Smallest fix: the vault's address is read by nothing as a constant, 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 in deployment.json instead of computing it), which no one else can front-run; or keep CREATE2 but through a deployer that mixes msg.sender into the salt. Either way `_record` already handles a vault address that is only known after the broadcast.","line":212,"path":"script/DeployMainnet.s.sol","reproduction":"On any fork after stage one (deploy/mainnet/rehearse-fork.sh up to 'Stage one deployed'): from anvil account 2, `cast send 0x4e59b44847b379578588920cA78FbF26c0B4956C $(cast concat-hex <SALT_VAULT> <_vaultInit(p)>)` where SALT_VAULT = keccak256('infer-protocol/mainnet/v1/ParameterizedVault') and the initcode is `forge inspect ParameterizedVault bytecode` ++ abi.encode(STAKED_IMD, address(0), WORK_ORACLE_SENTINEL, priceFeed, nhiFeed, spotFeed) from deployment.json. EXPECTED (the design's stated property): the vault cannot exist until runVault(). ACTUAL: code lands at the address `plan().vault` reports, `ParameterizedVault(p.vault).treasury().vault() == p.vault`, and a subsequent `runVault()` logs 'exists, skipped' for the vault. With a raced off-pool first value relayed first, `lock` + `draw` succeed against it in the same block, and runVault() then reverts in verifySeeded while the vault with positions remains at the planned address. In-repo check of the mechanism: _deploy (lines 250-260) skips any address holding code and `_refuseUnlessReady` checks nothing about who deployed what is already there; test/ManifestConstructible.t.sol shows the vault constructor runs for an arbitrary creator.","severity":"low","snippet":"        _deploy(SALT_VAULT, _vaultInit(p), p.vault);","title":"DeployMainnet: the vault is NOT absent between the two stages; anyone can CREATE2 it at the planned address from the public initcode as soon as stage one lands, so a raced first value can be priced an"},{"citation":"resolved","description":"CLAIMS WITHOUT THE PROPERTY (Q6). (1) CDPVault 330: 'cold capital, and a bank, left untouched this long count in full'. For a bank the opposite happens: `_cool` returns zero for it after a day (line 1023), so capital returning to a bank untouched for a day is credited NOTHING and counts as fully cold; 'count in full' describes the cold side only. (2) 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 but not here. (3) script/DeployMainnet.s.sol 183-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 section 7 paragraph 2: the absence is not enforced (low finding, DeployMainnet 212). (4) CDPVault 183-184 and Parameters 110-112, 'Each redemption raises the fee base by redeemed / supply / this': since retry2 the denominator is the WARM supply floored at 1,000 (`_feeBase`), not the supply; at launch a 45 imdUSD burn reaches the cap at divisor 1, not '4.5% of supply'. (5) CDPVault 891 `prior == 0 ? cap` is dead since the floor (prior is never below 1,000e18); harmless. (6) docs/MAINNET-RUNBOOK.md section 2 'What the keeper needs funded' still says 'Treasury.fundOracle pays only from sIMD the Treasury holds' in section 7 step 4 and then corrects itself in the same paragraph; and section 5's 'forge test # expect 362+ pass' is stale (599). Everything else checked reads true, including the rewritten lag comments (318-328, 938-948, 1012-1020, 997-999), the accepted-premium bound at 758-766 (checked against retry2 #5: (supply - fresh) / (supply - fresh - repaid), repaid bounded by principal above half the collateral's value, below par only, gas only for a churner at mat; a churner below mat cannot redraw), ParameterizedVault 230-245, the four transient slots (246-259), `_backingPerUnit` 743-766, cover 554-566, bite 1158-1176, Treasury.fundOracle 493-503, OracleAsker 31-63, SwarmFeed 7-52 and 407-444, UsdPriceFeed, SharePriceFeed, ImdUSD, Governed, Parameters 54-66.\n\nANSWERS WHERE NOTHING IS WRONG. Q1(a): `grep -n excess src/` finds only feeExcess; Position.excess, _excess, _excessNow and _moveExcess are gone and `free`, `draw`, `wipe` carry no residue. REPAID_THIS_TX_SLOT is added in `_payDebt` (wipe, bite) and in `cover` and read only by `_backingPerUnit`: a same-call wipe W (warm, in the 170-200% band) / cash / draw W reads supply = (S - W) + W = S in the live figure and leaves `fresh` and the warm secured term unchanged in the lagged one, so the churn is paid the honest figure (test/scratch/Probe.t.sol test_sameCallWipeCashDrawIsClosed: honest 1293.1875 IMD, churned through a contract 1293.07, i.e. 0.01% LESS because the wipe lowers the `prior x mat` cap). A same-call wipe of COLD principal double-counts W in the lagged denominator (supply + W, fresh - W), the lower-payout direction. The accepted cross-transaction premium is bounded as stated at 760-762. Q1(b): position.coldAt is written only by that position's `_lag` and `_coldAt` by every `_lag`, so position.coldAt <= _coldAt always and the position's elapsed is at least the total's; with one cutoff (BACKING_WARMUP) for both, a total that reads zero implies every position reads zero, and a position's `coldOut = min(position.cold, out)` with position.cold floor-rounded over the longer elapsed never exceeds what the total holds for it (up to _pow's 1e-27 truncation, the retry2 info). So no decrease can retire cold the total does not hold for that position, which is the only path by which one position's capital could warm another's; test/final-vault-panel/QuietDayTransfer.t.sol and t","line":330,"path":"src/CDPVault.sol","reproduction":"(1) Read `_cool` line 1023 against line 330: `if (elapsed >= BACKING_WARMUP) return 0;` is applied to the bank at lines 971-975, so a bank untouched a day returns zero credit. (2) Compare lines 589-591 with 655-656. (3) The low finding at script/DeployMainnet.s.sol:212. (4) `_feeBase` lines 1047-1053 against the sentence at 183-184. (5) `_feeBaseFloor` returns 1_000e18, so `prior` at 891 is never zero. (6) docs/MAINNET-RUNBOOK.md section 7 step 4 and section 5 step 1 against `forge test` (599 passed per the final-vault-panel resolution).","severity":"info","snippet":"    /// @dev A day: cold capital, and a bank, left untouched this long count in full (`_cool`).","title":"Comments and NatSpec that claim properties the code does not have at 6085c8a; the answers to Q1-Q6 where nothing is wrong; what was read"}],"hash":"d41577c5b95f9f7d0edc2599daa6dc5f0f6e549de4708c27237e12749591cc82","nodeId":"73eeac04-fad7-4521-83ad-104e21139b82","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds three findings; nothing in `src/`, `script/` or `deploy/` was changed.\n\n## What I found\n\n**Low — `CDPVault._feeBaseFloor` (src/CDPVault.sol:1057).** The 1,000 imdUSD floor closes final-panel finding #6 for dust only. At divisor 2, a 90 imdUSD reserve-funded burn against the floored base is the whole cap (90/1,000/2 = 4.5%), and `cash` stores it unchanged. Reproduced: 12 s after a 100,000 draw, `cash(90e18, 0, address(0))` sets `redemptionBaseRate == 0.045e18`; the 1 imdUSD quote reads 210 bps at +18 h, 163 at +24 h, 79 at +48 h — the exact figures the resolution says are fixed. Cost to the pinner: $4.05. Proof attached (`test/scratch/Proof_FloorPin.t.sol`, fails on this commit). Smallest fix: bound the *stored* increase by the burn's share of the live supply while still charging the redeemer against `prior`.\n\n**Low — DeployMainnet between stages (script/DeployMainnet.s.sol:212).** The two-stage deploy's safety property (\"the vault is absent until the first values are checked\") is not enforced: the salt and initcode are public and the canonical CREATE2 deployer is permissionless, so anyone can land the correct vault at `p.vault` the moment stage one finishes, relay a pumped first value, and draw against it before `verifySeeded` runs. No user funds at stake and no attacker profit (no imdUSD market), so it is a griefing/re-salt vector. Fix: deploy the vault with plain CREATE from the throwaway deployer — its address is not a source constant, so it never needed planning.\n\n**Info — comments without the property, Q answers, coverage.** Notably CDPVault:330 (a bank \"counts in full\" after a quiet day — it is in fact empty), the unqualified `cover` claim at 589–591, and the \"vault absent\" claims in the script, SwarmFeed and runbook.\n\n## Where nothing is wrong\n- **Q1(a)** The excess is fully gone; the same-call wipe/cash/draw churn is closed (contract churn paid 0.01% *less* than honest); the accepted premium bound at 760–762 holds.\n- **Q1(b)** `position.coldAt ≤ _coldAt` always, so with one cutoff a zero total implies zero positions; `coldOut` never exceeds the total's share for that position. The reverse (stale position, busy total) only understates — confirmed by probe.\n- **Q2** No extracting or desynchronising cross-subsystem sequence found; `cover`, `bite` and `relayAndBite` interactions with the reserve and fee base are sound.\n- **Q3** Over-borrowing needs ~4 h of the pool held 40–100% above market (~$1M ETH, ~$25k fees) for ~$130k gross at LINE $1M — and only with an imdUSD market at par, which launch lacks. Liquidation timing is cheap (~$250 of fees for a position within 1% of mat). NHI is not pool-derived.\n- **Q4/Q5** Halts, recoveries, runbook vs code, and the deploy checks are laid out in the info entry; the one deploy-wrong-and-pass case beyond the low is a body template that disagrees with the feed's pinned prefix (caught only by the manual `check-bodies.mjs`).\n\nRead in full: every shipped contract in `src/`, the deploy script and preflight, `plan.py`, the body templates, the runbook, and the relevant audit docs. Not reached: live sIMD/IMD code, the Intake, the keeper.","treeHash":null,"usage":{"cachedInputTokens":4080049,"inputTokens":559,"model":"claude-fable-5-1","outputTokens":79718,"runtime":"claude","turns":42,"wallClockMs":1178337}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e12f98dda6acc55f","findings":[{"citation":"resolved","description":"Stage two deploys the vault through the canonical deterministic deployer (0x4e59b44847b379578588920cA78FbF26c0B4956C, forge-std CREATE2_FACTORY), which takes `salt ++ initcode` from ANY caller and lands the contract at an address that depends only on (deployer, salt, initcode). Both inputs are in the committed deploy source: SALT_VAULT (line 96) and `_vaultInit(p)` = type(ParameterizedVault).creationCode ++ abi.encode(STAKED_IMD, 0, WORK_ORACLE_SENTINEL, p.price, p.nhi, p.spot) (lines 485-492), where p.price/p.nhi/p.spot are the stage-one addresses. The vault's constructor needs only STAKED_IMD, the three feeds, TREASURY_FACTORY and WORK_ORACLE_FACTORY to have code, all of which exist after `run()`. So from the moment stage one lands, any EOA can (1) relay a first value for price and spot off a pumped pool (a feed's first value is bounded by nothing on chain and SwarmRelay is permissionless, as the comments on verifySeeded at lines 371-382 say), (2) call the deployer with SALT_VAULT ++ _vaultInit(p), which puts the vault at p.vault, live from its constructor, and (3) lock sIMD and draw up to LINE against the pumped value in the same block. verifySeeded never ran: it is a view the OPERATOR runs before THEIR broadcast, and nothing in the vault or the deployer checks it. The operator's later `runVault()` prints `exists, skipped` for the vault (line 251-254) and `verify()` then fails on `vault.totalDebt() == 0` (line 307), so the planned address, the keeper's and the terminal's configuration and the announcement are burnt and the vault must be re-salted and redeployed; if the operator instead proceeds with the address, the protocol opens with an under-collateralised position holding the line. The cheap form is griefing: ~12.7M gas (about 0.013 ETH at 1 gwei) plus one honest 1-wei draw forces a re-salt, repeatable for every public salt. The expensive form is the over-borrow the stage split was meant to close (sweep panel audit, oracle, low): the attacker holds the pool up for the primary's window median and the spot's last block (about 35 minutes at 1% of the push both ways), relays, deploys, draws. Reachable with the constants as committed, on mainnet, with no key. Smallest fix: do not deploy the vault through a public-salt CREATE2. Nothing reads the vault's address as a source constant (it is layer 5; only the record and the keeper read it from deployment.json), so `runVault` can use a plain `new ParameterizedVault(...)` from the throwaway deployer, or a CREATE2 factory that binds the salt to msg.sender (e.g. the first 20 bytes of the salt equal to the caller, as 0age's ImmutableCreate2Factory requires), and `plan()`/`_record` take the vault address from the deployment rather than computing it. Keep the stage split: it is right, it just needs the second stage to be the operator's alone.","line":212,"path":"script/DeployMainnet.s.sol","reproduction":"test/scratch/Lead_VaultCreate2FrontRun.t.sol (passes on this code, demonstrating the gap). Stage one as deployed: TreasuryFactory etched at TREASURY_FACTORY, WorkOracleFactory at WORK_ORACLE_FACTORY, a Chainlink-shaped aggregator at CHAINLINK_ETH_USD, three unseeded feeds, no vault. ATTACKER (an EOA with 1,000,000 IMD) sets the price and spot feeds to $3 where the market is $1 (a raced first value), NHI 0.85, then calls the canonical deployer with bytes.concat(SALT_VAULT, type(ParameterizedVault).creationCode ++ abi.encode(gem, 0, WORK_ORACLE_SENTINEL, price, nhi, spot)). EXPECTED (the runbook's and the sweep-panel resolution's claim: 'with the vault absent until the first values are checked, a raced first value can price nothing'): the call fails or lands somewhere the operator's plan does not name. ACTUAL: the call succeeds and returns exactly vm.computeCreate2Address(SALT_VAULT, keccak256(initcode), 0x4e59b4...) == the planned p.vault; ATTACKER then lock(340_000e18) and draw(600_000e18) succeed in the same transaction sequence (totalDebt == 600,000e18); at the honest $1 the position's collateralRatio() is 56 (< 60): $600k of imdUSD against $340k of collateral in the vault at the address the operator planned to announce. The operator's runVault() would then skip the deploy (code exists) and verify() would revert 'vault: opens with debt or a work ceiling'.","severity":"medium","snippet":"        _deploy(SALT_VAULT, _vaultInit(p), p.vault);","title":"DeployMainnet.runVault: the vault's CREATE2 salt and initcode are public, so anyone can deploy ParameterizedVault at its planned address between stage one and stage two and draw against a raced first "},{"citation":"resolved","description":"The final-panel low #6 (a dust reserve-funded redemption right after launch stored the 4.5% cap as redemptionBaseRate, decaying with a twelve-hour half-life while the cold supply warmed with a six-hour one) is marked 'Fixed' by flooring `_feeBase()` at 1,000 imdUSD. The floor changes the price of the pin, not its existence: `_redemptionRate` stores `amount / prior / divisor` capped at 4.5%, and with prior at the floor and divisor 2 any burn of at least 90 imdUSD stores the cap. The reserve route is open to anyone (redemptionReserve is gem.balanceOf(treasury), listed or not, so a transfer of a few sIMD to the Treasury opens it, and the burn is paid back in sIMD less the fee), so the pin costs 5% of 90 imdUSD, $4.50, plus gas, and holds while the warm supply is below 1,000 imdUSD: for a $100k first draw that is only the first minutes (warm supply passes 1,000 about five minutes after the draw), for a $10k first draw about an hour, and again whenever the warm supply turns over. The effect is the one the panel measured: the next 1-imdUSD quote reads 500 bps at once, 369 bps six hours later and 163 bps a day later where the honest figure is near the 50 bps floor, i.e. the peg floor min(1 - fee, backing) sits at 0.955 instead of about 0.995 through the launch day, and redemptions, the mechanism that defends the peg, are deterred when most supply is new. Not an attack on funds; a quantified launch cost that the NatSpec at lines 1043-1046 presents as closed. Reachable with the constants as committed. Smallest fix: either state the residual at the line and in docs/MAINNET-RUNBOOK.md section 7 (open redemptions a day after the first draws), or make the floor proportional to the live supply (e.g. max(1,000e18, totalSupply / 10)), accepting the bounded dilution of the STORED rate that a cold draw then buys (at most 10x of the honest increase, against the 4.5% cap), or store the increase measured against max(prior, live supply / 2) while still charging the redeemer against prior.","line":1057,"path":"src/CDPVault.sol","reproduction":"test/scratch/Lead_LaunchLag.t.sol test_feeFloorStillLetsNinetyImdUsdStoreTheCap (logs). ParameterizedVault over MockIMD at $1 (Chainlink 2000e8 etched), NHI 0.85, launch constants (divisor 2). OTHER lock(400_000e18), draw(100_000e18), hands DUSTER 200 imdUSD; the Treasury holds 1,000 IMD (the reserve route). Twelve seconds after the draw DUSTER cash(90e18, 0, address(0)). EXPECTED (the resolution's claim: a dust redemption 'cannot store the cap for everyone'): a stored rate near the honest increase for 90 of 100,000 (about 4.5e14, 0.045%). ACTUAL: redemptionBaseRate == 45000000000000000 (the 4.5% cap); redemptionFeeBps(1e18) == 500 at once, 369 after 6 hours, 163 after 24 hours. Cost to the pinner: the 5% fee on 90 imdUSD ($4.50) and gas.","severity":"low","snippet":"        return 1_000e18;","title":"CDPVault._feeBaseFloor: the 1,000 imdUSD floor moves the launch-day pin of the stored base rate from dust to 90 imdUSD; a $4.50 fee still stores the 4.5% cap for every redeemer for a day"},{"citation":"resolved","description":"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 fresh collateral (bounded by its own principal, held across one transaction) as backing. A newcomer can therefore lock, draw (cold) in one transaction, cash from the reserve in the next at par where the honest post-exit backing is below it, then repay and withdraw: 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. In the reproduction (reserve 20,000 IMD, launch supply 100,000, IMD halved so the lone position is at 85%: honest backing 0.95) the figure a redeemer is paid against reads 1.0 at 12 seconds, one hour and six hours after launch, 0.984 at twelve hours and 0.9504 at a day. The loss is (paid - honest) x the reserve-funded amount, at most the reserve x the gap below par, and it needs a seeded reserve and a below-par book within the first hours; the runbook does not seed the reserve (its only sIMD income is liquidation cuts and cover sweeps), which is why this is low rather than medium. The NatSpec at lines 753-755 ('honest redemptions are not underpaid ... When every unit of supply is fresh there is no lagged figure and the live one stands') describes the all-fresh case; the half-warm case is the one above. Reachable with the constants as committed. Smallest fix, no new storage: apportion the reserve to the warm supply pro rata in the lagged figure, reserve * (supply - fresh) / supply, which is exact when the fresh supply is the newcomer's and errs toward underpaying (the lag's safe direction) when an honest cold draw dilutes it for a day; or, operationally, do not seed the Treasury's sIMD before the launch supply has warmed (a day) and say so in docs/MAINNET-RUNBOOK.md section 7.","line":775,"path":"src/CDPVault.sol","reproduction":"test/scratch/Lead_LaunchLag.t.sol test_reserveCreditedToWarmSupplyAloneAtLaunch (logs). ParameterizedVault over MockIMD at $1 (Chainlink 2000e8 etched), NHI 0.85, wage 0. OTHER lock(170_000e18), draw(100_000e18), hands ATTACKER the 100,000; the Treasury is given 20,000 IMD; IMD to $0.50 (OTHER at 85%, re-priced by lock(1)); honest backing = (20,000 + 170,001) x 0.5 / 100,000 = 0.950005e18 and backingPerUnit() reads 0.95e18 before the newcomer. 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); twelve seconds later read backingPerUnit(), the figure a cash(amount, 0, address(0)) from the reserve is paid against. EXPECTED: at most the honest 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. The round trip (cash from the reserve, wipe, free) then takes the reserve at par while the book is backed at 0.95.","severity":"low","snippet":"            uint256 lagged = Math.mulDiv(reserve + _securedCollateralValue(price, true), 1e18, supply - fresh);","title":"CDPVault._backingPerUnit: the lagged figure credits the whole reserve against the warm supply alone, so for the first hours after launch a newcomer's fresh capital lifts a reserve-funded redemption to"},{"citation":"resolved","description":"Lines 501-503 say 'sIMD's one-block hold applies: shares that arrived in this block cannot be withdrawn in it, so a call right after a liquidation reverts and succeeds a block later.' Lines 547-558 do the opposite on purpose: the unwrap runs in `this.unwrapForOracle{gas: UNWRAP_GAS}` inside try/catch, a refused withdraw sets fromShares = 0, and the call returns `plain` (or 0) without reverting, which is the sweep-panel info fix (a one-raw-unit share sent every block must not hold the oracle's funding off). The notice describes the pre-fix behaviour. Smallest fix: 'so a call in the block a liquidation landed in sends only the Treasury's plain IMD and unwraps nothing; the shares are unwrapped by the next call.'","line":502,"path":"src/Treasury.sol","reproduction":"Read Treasury.fundOracle lines 501-503 against 547-558: `try this.unwrapForOracle{gas: UNWRAP_GAS}(IERC20(token), fromShares) {} catch { fromShares = 0; }` followed by `sent = plain + fromShares; if (sent == 0) return 0;` — no revert path exists for the hold.","severity":"info","snippet":"    /// arrived in this block cannot be withdrawn in it, so a call right after a liquidation reverts and","title":"Treasury.fundOracle NatSpec claims a call right after a liquidation reverts on sIMD's one-block hold; the code catches the unwrap and returns what it could send"},{"citation":"resolved","description":"Lines 753-754 claim honest redemptions are not underpaid 'because new debt and the imdUSD minted against it are excluded together'. Two accepted behaviours documented elsewhere in the file underpay honest redemptions for hours: lines 325-327 (a price fall re-prices a debt-bound term upward and the rise is cold, so long-held collateral is excluded from the lagged numerator while its debt stays in the denominator, 'accepted, the lag's safe direction') and lines 1017-1019 (a position untouched for a day reads zero while a busy total still holds its share, so a later repayment leaves that share orphaned in the total, 'only understates the lagged figures ... accepted'). Both lower the lagged figure every redeemer is paid against. The claim at 753-754 is true only of the mechanism it names (the newcomer's own capital). Smallest fix: 'honest redemptions are not underpaid BY THE NEWCOMER'S CAPITAL, because ...; the lag's other approximations (lines 325, 1017) err toward underpaying and are accepted.'","line":753,"path":"src/CDPVault.sol","reproduction":"Read lines 753-754 against 325-327 and 1017-1019; the retry2 panel's reproductions of those two accepted items (docs/AUDIT-RETRY2-PANEL-VAULT-2026-10-08.md, #6 and #7) show the underpayment, and test/scratch/Lead_LaunchLag.t.sol shows the opposite direction at launch.","severity":"info","snippet":"    /// figure but not the lagged one, whichever position it sits in (`_lag`); honest redemptions are not","title":"CDPVault._backingPerUnit NatSpec: 'honest redemptions are not underpaid' is contradicted by two accepted items in the same contract (a price fall's upward re-pricing counts as cold; a position untouch"},{"citation":"resolved","description":"`_redemptionRate` computes the increase as amount / prior / divisor where prior is `_feeBase()`: the live supply plus warm repaid principal, less cold principal and work minted this transaction, floored at 1,000 imdUSD (lines 1047-1053). Three comments still describe the denominator as the supply: CDPVault line 183 ('redeemed / supply / this'), Parameters lines 110-112 ('redeemed / supply / divisor. At 1 a run reaches the 5% cap after 4.5% of supply; at 8 it takes 36%') and DeploymentConfig line 127 ('redeemed / supply / this. At 2, redeeming 10% of supply at once costs 5.0%'). Right after a large draw the two differ by orders of magnitude (the fee-floor finding: 90 imdUSD of a 100,000 supply is 0.09% of the supply and 9% of the base). Also 'raises the fee base' means the base RATE (redemptionBaseRate), while `_feeBase` is now the name of the denominator. Smallest fix: 'raises the base rate by redeemed / warm fee base (`_feeBase`) / this'.","line":183,"path":"src/CDPVault.sol","reproduction":"Read CDPVault lines 183-184, 885-893 and 1047-1053; Parameters lines 110-112; DeploymentConfig line 127-128. test/scratch/Lead_LaunchLag.t.sol test_feeFloorStillLetsNinetyImdUsdStoreTheCap: 90 of a 100,000 supply stores the cap, which 'redeemed / supply / 2' (0.045%) does not predict.","severity":"info","snippet":"    /// @notice Each redemption raises the fee base by redeemed / supply / this. The base vault reads","title":"redemptionDivisor NatSpec (CDPVault, Parameters, DeploymentConfig) says the base rate rises by 'redeemed / supply / divisor'; since the retry2 fix and this commit's floor the denominator is the floore"},{"citation":"resolved","description":"DRIFT_FALL_TRIGGER_OF_CAP_BPS = 2500 and DRIFT_RISE_TRIGGER_OF_CAP_BPS = 0 (DeploymentConfig lines 258-259), so OracleAsker.triggerBps returns (500, 0) at a 2,000 bps cap and `_drifted` pays only when the pool is below the feed by more than 5% (OracleAsker lines 362-372); DeployMainnet.verify pins exactly that (line 322). The runbook's own section 1 (2b) and section 7 step 2 say a quarter of the cap and 5%; section 6 still says 10% from the former symmetric half-cap trigger. Smallest fix: '(the asker asks on a FALL of 5%, a quarter of the cap, never on a rise)'.","line":243,"path":"docs/MAINNET-RUNBOOK.md","reproduction":"Compare docs/MAINNET-RUNBOOK.md line 243 with src/DeploymentConfig.sol lines 258-259, src/OracleAsker.sol lines 362-372 and script/DeployMainnet.s.sol line 322.","severity":"info","snippet":"`maxDeviationBps` **2000** (the asker asks on drift at 10%); the asker treats price and spot as","title":"docs/MAINNET-RUNBOOK.md section 6 says the asker asks on drift at 10%; the committed asker pays for a FALL of a quarter of the cap (5%) and never for a rise"}],"hash":"e05d723760896697b6c4a5b37de15eb822acb440e4de36708791f44ec42c4158","nodeId":"43016506-ef05-4c51-9789-869875334025","outcome":"completed","summary":"I've recorded seven findings and confirmed each with a scratch test or a direct source comparison. Final report follows.\n\n## Findings (written to `.imd-findings.json`)\n\n| # | Sev | Where | Finding |\n|---|---|---|---|\n| 1 | **medium** | `script/DeployMainnet.s.sol:212` | The vault's CREATE2 salt and initcode are public, so **anyone can deploy ParameterizedVault at its planned address the moment stage one lands** and draw against a raced first value — the exact gap the two-stage deploy was built to close. `verifySeeded` is only a view the operator runs; nothing on chain gates the vault's creation. Confirmed in `test/scratch/Lead_VaultCreate2FrontRun.t.sol`: an EOA lands the vault at `p.vault`, locks $340k, draws $600k at a $3 raced price (56% CR at the honest $1). The operator's `runVault` then skips (code exists) and `verify` fails on `totalDebt == 0` — forced re-salt for ~0.013 ETH, repeatable. Fix: plain `new` from the deployer, or a sender-bound CREATE2 factory; nothing reads the vault's address as a constant. |\n| 2 | low | `src/CDPVault.sol:1057` | The 1,000 imdUSD fee-base floor moves the launch-day pin from dust to **90 imdUSD**: a $4.50 fee still stores the 4.5% cap for everyone (quotes 500 → 369 → 163 bps over a day). |\n| 3 | low | `src/CDPVault.sol:775` | The lagged figure credits the **whole reserve against the warm supply alone**, so for ~12h after launch a newcomer's one-transaction capital lifts reserve-funded redemptions to par while honest backing is 0.95 (D1 round trip, bounded by the reserve × gap). |\n| 4 | info | `src/Treasury.sol:502` | `fundOracle` NatSpec claims a same-block call reverts on sIMD's hold; the code catches the unwrap. |\n| 5 | info | `src/CDPVault.sol:753` | \"honest redemptions are not underpaid\" contradicts the two accepted underpaying approximations (lines 325, 1017). |\n| 6 | info | `src/CDPVault.sol:183` (+ Parameters 110, DeploymentConfig 127) | \"redeemed / supply / divisor\" — the denominator is the floored warm fee base. |\n| 7 | info | `docs/MAINNET-RUNBOOK.md:243` | \"asks on drift at 10%\" vs the code's 5%, falls only. |\n\n## Answers to the numbered questions\n\n**1(a).** Nothing of 58f73de remains (`grep excess` finds only `feeExcess`/`ExcessRepayment`/`ExcessDeviation`). `REPAID_THIS_TX_SLOT` is added in `_payDebt` (wipe, bite) and `cover`, read in `_backingPerUnit` only: a same-call warm wipe W leaves `supply` and `fresh` unchanged, so wipe/cash/draw pays no premium; a cold wipe raises `supply − fresh` and underpays (safe). The cross-tx premium bound holds in its general form (W ≤ principal − collateral·price/2) — note `wipe` has no health check, so below mat W can exceed 15%, but no redraw is possible there and it costs real repayment, so the stated reason stands.\n**1(b).** `position.coldAt ≤ _coldAt` always, so a position's elapsed ≥ the total's; with the uniform cutoff, \"total reads zero ⇒ every position reads zero\", totals round up and positions down, and `coldOut ≤ position.cold ≤ its share`. The only leak is `_pow` truncation between piecewise and single cooling (~1e-27 relative, ≤ thousands of raw sIMD wei) — not economic. The reverse (stale position, busy total) only orphans cold in the total → understates.\n**1(c).** No dilution past the floor: the floor binds only while the warm base < 1,000, which the attacker cannot cause for others' supply (own cold draws are netted out). The single read is consistent for charge and stored rate. Residual: finding 2.\n\n**2.** All closed same-tx churns hold; the per-tx tallies err toward understating in every combination I traced (lock/free/lock, draw/wipe/draw, bite→cash, cover→cash, earn→cash). The bank conserves warmth per position and never creates it (re-lock after drain is at most neutral vs. never being bitten). Only finding 3 (launch window) crosses the boundary.\n\n**3.** Cheapest over-borrow: 20%/epoch, primary a window median → ≥7/13 samples pumped (~35 min/step) plus spot; walk-away profit needs ≥1.7× (3 steps, ~3h break-even; 2.07× in 4","treeHash":null,"usage":{"cachedInputTokens":4448210,"inputTokens":552,"model":"claude-fable-5-1","outputTokens":105285,"runtime":"claude","turns":40,"wallClockMs":1481705}}],"verification":[]}