{"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":"6229d0fc-c6a5-4c3e-bb56-64461d50b1cb","kind":"audit","nodes":[{"acceptedSubmissionHash":"0d0d77da090edd2f73967ce34f53fdac7c7b05df0b620fc9882880decbf49641","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":"49bc26e7e1e7585c0ca97f6eb73a1b4b935101b987c3cfa37b9699ab42d3ba71","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":"197c469eab1ddb97b7fc9fd219d7b866ac00ae39ca99a6439330f3ee1a77302d","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"4481e88ec4a67e4396919e8883fcff9d212736cb6bd1c93a61423a6010ed3166","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":"0db00ed943bb2c4a0fa3d2409b6ed4054df9f17f57d7d3ac4bf224ce6692e87d","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 protocol: every contract in src/, script/DeployMainnet.s.sol, script/DeployPreflight.sol, deploy/mainnet/ and docs/MAINNET-RUNBOOK.md section 7, at the pinned commit, for a mainnet launch. Seventeen audit rounds and their fixes are in (docs/AUDIT-*.md; the chain is web/content/docs/reference/audit-history.md). This is the LAST review before mainnet, of the WHOLE PROTOCOL at the commit that will deploy: every contract in src/, the deployment and the launch runbook, each mechanism in turn. Since the previous sweep (e4baedf, docs/AUDIT-FINAL-SWEEP-3-2026-10-09.md): the paced debt's netting reverted to the transaction's own mint, so the figure errs low, never high; the Treasury's paying functions gained a transient reentrancy guard; comments restated: git diff e4baedf c7d50ee -- src script. ACCEPTED items, each with its bound stated where it lives, are findings only if the stated bound is wrong or the reason does not hold: liquidation at a held-down pool (CDPVault.bite: 1.2/(1-push) of the debt at the real price, from the borrower; a thin position's remainder as bad debt), the payout price's gain per hour of hold and its lag after a rise or an honest fall (PAYOUT_PRICE_FALL_BPS_PER_HOUR), the dip and stale-term read (the paced figures), the paced debt erring low (_tallyPrincipalRetired), the fee-base floor, the work ceiling as an aggregate once the wage is on, the oracle's walk cost (docs/PARAMETERS-2026-10-05.md). Rank severity by what a finding lets someone take or block with the constants as committed (LINE $1M, wage 0, mat 170 at NHI >= 0.85, grace 6 hours, CHOP 20%, the backing's rise 2 points of par an hour, the follow 10% an hour, the payout price's fall 1% an hour, fee floor 100,000, fee cap 5%, FEED_MAX_DEVIATION_BPS 2000, SKEW_BPS 500, TIMELOCK 48 hours).\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. The oracle's feeds read one full-range Uniswap v4 pool on Ethereum, about $2.3M a side with a 1% fee; IMD also trades in other pools and on Base and Robinhood Chain, so a price held off-market in the oracle's pool is open to arbitrage from those venues. docs/PARAMETERS-2026-10-05.md has the numbers every economic parameter was chosen from.\n\nAnswer each numbered question, including the ones where nothing is wrong:\n1. THE ORACLE (SwarmFeed, PriceFeed, SpotFeed, NhiFeed, UsdPriceFeed, SharePriceFeed, SwarmRelay, OracleAsker): attestation checks (signer, domain, question binding, window, replay), the epoch and deviation rules after silence, the first value, Chainlink staleness and failure, the asker's triggers, budget, back-off and callback, relay bundles. Anything that lands a value the question does not support, holds a feed off, or spends the Treasury's budget for nothing.\n2. THE VAULT'S BORROWING AND LIQUIDATION (lock, lockIMD, free, draw, wipe, bark, barkFor, heel, bite, cover, the stability fee, dust, bad debt, the debt ceiling): every ordering by one or several positions and the relay; anything that leaves debt unbacked, frees collateral a position needs, stops a liquidation that should happen, or takes more than the stated bounds.\n3. REDEMPTION AND THE PACED FIGURES (cash, the paced backing, supply, debt and payout price, resecure, the fee base and ratchet, the reserve route): anything that pays a redeemer more than the honest backing at the paid price, or blocks honest redemptions beyond the stated lags.\n4. THE TREASURY (sync, withdraw, payStream, fundOracle, redeemIMD, the reserve register and its valuation, launch fees, the new guard): every exit bounded as documented, bad debt first, nothing an outsider can take, freeze or mis-record.\n5. GOVERNANCE AND MINTING FROM WORK (Parameters, the timelock and every bound, Governed, SwarmWorkOracle, WorkOracleFactory, earn, earnLine, backedDebt): anything a governor can do beyond the bounds or faster than 48 hours, and anything that mints work against backing that is not there if the wage is turned on.\n6. IMDUSD, THE FACTORIES AND THE DEPLOYMENT (ImdUSD, TreasuryFactory, DeployMainnet.run, verifySeeded, runVault with VAULT_SALT, verify, plan.py, the pinned bodies) and the launch window hour by hour against the runbook: what can be deployed wrong and pass, what a stranger can do between the stages, every way the protocol can halt on day one and how each recovers.\n7. REENTRANCY AND EXTERNAL CALLS across every contract: each external call, what it can call back into, and whether state is written before it.\n8. Every comment, NatSpec or runbook line that claims a property the code does not have, and the list of what you read in full and what you could not reach.\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":"caf1d81d5cfb6e45d4dc95496d627c4c9e75966a1efc768b7a372b55c298f191","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"6229d0fc-c6a5-4c3e-bb56-64461d50b1cb","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":"52158","feedbackHash":"133b10bae67e592a6b26b38cccfd87e06921b00a440daba9fad206eceaaed3db","nodeKey":"audit_economics","submissionHash":"0d0d77da090edd2f73967ce34f53fdac7c7b05df0b620fc9882880decbf49641","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52163","feedbackHash":"a2f3d7e8ab478a37962da903ac0386ac89df3a10755ec6a2be9c1d83197e1413","nodeKey":"audit_flow","submissionHash":"49bc26e7e1e7585c0ca97f6eb73a1b4b935101b987c3cfa37b9699ab42d3ba71","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51163","feedbackHash":"b0c7101d54357734dba759b9448edf1fb36663c2072b38da65dec47ec6680ffd","nodeKey":"audit_judge","submissionHash":"197c469eab1ddb97b7fc9fd219d7b866ac00ae39ca99a6439330f3ee1a77302d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52256","feedbackHash":"e4d8cc0c83a4210b2d20ab8e0b11fc6417057627f347d98fd4741d45c9bed2bf","nodeKey":"audit_math","submissionHash":"4481e88ec4a67e4396919e8883fcff9d212736cb6bd1c93a61423a6010ed3166","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51509","feedbackHash":"58cf6c018da0c886f498793ffcc718fc2e094cc5a7cf143068bae03cc27fc028","nodeKey":"audit_permissions","submissionHash":"0db00ed943bb2c4a0fa3d2409b6ed4054df9f17f57d7d3ac4bf224ce6692e87d","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"7189a8179db21fff70ff8f8284018e3d91e670508286ee27be3a20fef1e8ff00","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4dd67dae195771b6","findings":[{"citation":"resolved","description":"Oracle (question 1: 'holds a feed off'). `_epoch()` anchors every new epoch at `_value`, whatever was last accepted, with the fresh allowance (maxDeviationBps, 20%) unless the value has been stale a whole STALE_GROWTH_PERIOD past maxAge (two hours after acceptance). The epoch bound protects against a WALK, but nothing lets an honest value UNDO a move made just before an epoch expired: a value accepted at t0+59min inside an epoch anchored at V (so up to 20% off V) is the anchor of the epoch that opens at t0+60min, and the honest V is then 25% above a 0.8V value and refused `ExcessDeviation` until t0+59min+2h. For the spot feed this costs one push: the spot recipe reads the window's last block alone (SpotFeed NatSpec, 'samples 1'), the buyer chooses toBlock (any block within 300 of head, span 150..1200), and a panel attests a pushed end-of-block pool state honestly. So: (1) relay any honest spot at t0 (0.5 IMD; or wait for a keeper refresh and read `epoch()`); (2) at about t0+50min sell ~24k IMD into the v4 pool (a 19-20% fall, about $5k in round-trip fees at launch depth: the hold is one block, restored next block, so there is no arbitrage to absorb), buy a spot attestation with toBlock at that block, relay it at t0+59min (within the live epoch's 20% of V); (3) from t0+60min every honest spot reading is refused for two hours, during the first of which the vault reverts `PriceDivergence` (spot 0.8V against a primary at V, SKEW_BPS 500) and during the second `StaleFeed` (the pushed spot's lifetime). `draw`, priced `free`, `cash`, `barkFor`, `heel`, `bite` and `resecure` all stop; a mark whose one-hour bite window falls inside the halt expires and must be retaken with a fresh six-hour grace; redemptions, the peg's defence, are closed. Repeatable: when the honest V lands at t0+59min+2h it opens a wide epoch anchored at 0.8V with `_epochFirst` = V, and the same push at that epoch's end re-anchors the next at 0.8V again, so about $5k per three hours (about $40k a day) keeps the vault halted. The accepted liquidation bound at CDPVault.bite lists 'the cost of the hold (... for seven hours every arbitrageur who buys the held pool cheap and sells elsewhere must be absorbed)' among its defences; with the primary's samples at predictable blocks (the 2026-10-06 internal audit's premise, unverifiable here) the same late-epoch timing on both feeds locks a pushed primary and spot in for the hour marks need, without holding the pool at all, so that reason does not hold for the price pushed in through the epoch's end. Reachable with the constants as committed (maxAge 1h, cap 2000, SKEW 500). Smallest fix: when a new epoch opens, also accept a value within maxDeviationBps of the expired epoch's anchor (`_anchorValue`, or `_value` for a first epoch) and anchor the new epoch at that anchor in that case, so a one-block push can be undone by the next honest reading and the sustained walk rate stays the cap per epoch (a reversal never extends the walk).","line":416,"path":"src/SwarmFeed.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// A value relayed in the last minute of a live SwarmFeed epoch becomes the next epoch's anchor. For the spot\n// feed, whose recipe reads one block, that is one end-of-block push of the pool (restored the next block),\n// attested honestly: the honest spot, 25% above a 0.8x push, is then refused ExcessDeviation until the pushed\n// value has been stale a whole hour (two hours after the push), and the vault refuses every price action\n// (PriceDivergence for the first hour, StaleFeed for the second). This test FAILS on the committed code at\n// the honest re-seed and passes once a value within maxDeviationBps of the expired epoch's anchor is accepted\n// when a new epoch opens (and anchors the new epoch there).\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {CDPVault} from \"src/CDPVault.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\nimport {PriceFeed} from \"src/PriceFeed.sol\";\nimport {NhiFeed} from \"src/NhiFeed.sol\";\nimport {SpotFeed} from \"src/SpotFeed.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {APPROVED_OPERATOR} from \"src/DeploymentConfig.sol\";\n\ncontract LockoutProofPriceFeed is PriceFeed {\n    constructor() PriceFeed(1 hours, 2000) {}\n\n    function seed(uint256 v) external {\n        _accept(v, uint64(block.timestamp));\n    }\n}\n\ncontract LockoutProofNhiFeed is NhiFeed {\n    constructor() NhiFeed(1 days, 2000) {}\n\n    function seed(uint256 v) external {\n        _accept(v, uint64(block.timestamp));\n    }\n}\n\ncontract LockoutProofSpotFeed is SpotFeed {\n    constructor() SpotFeed(1 hours, 2000) {}\n\n    function seed(uint256 v) external {\n        _accept(v, uint64(block.timestamp));\n    }\n}\n\ncontract SpotEpochLockoutProofTest is Test {\n    address private constant BORROWER = address(0xB0B);\n    uint256 private constant V = 4e15; // wei of ETH per 1e18 IMD: the honest spot and primary\n\n    MockIMD private imd;\n    CDPVault private vault;\n    LockoutProofPriceFeed private primary;\n    LockoutProofNhiFeed private health;\n    LockoutProofSpotFeed private spot;\n\n    function setUp() public {\n        vm.warp(1_000_000);\n        vm.roll(20_000_000);\n        imd = new MockIMD();\n        primary = new LockoutProofPriceFeed();\n        health = new LockoutProofNhiFeed();\n        spot = new LockoutProofSpotFeed();\n        vault = new CDPVault(address(imd), address(0), address(0), address(primary), address(health), address(spot));\n        vm.prank(APPROVED_OPERATOR);\n        imd.mint(BORROWER, 1_000_000 ether);\n        vm.prank(BORROWER);\n        imd.approve(address(vault), type(uint256).max);\n    }\n\n    /// @dev t0: an honest refresh (anyone's 0.5 IMD) opens a live spot epoch anchored at V. t0 + 59 min: the\n    /// attacker relays a spot attestation of 0.8 V, an honest reading of the one block the pool was pushed in\n    /// (inside the epoch's 20% of V, so accepted). t0 + 61 min: the epoch has expired; the honest V is within\n    /// 20% of the expired epoch's anchor and must land, so the vault's price actions resume. On the committed\n    /// code the new epoch anchors at 0.8 V with the fresh 20% allowance, V is 25% above it, and this reverts\n    /// ExcessDeviation; the vault then refuses draw, bark, bite and cash for two hours.\n    function test_honestSpotLandsOnceTheLatePushsEpochHasExpired() public {\n        health.seed(0.9e18);\n        primary.seed(V);\n        spot.seed(V);\n        uint256 t0 = block.timestamp;\n        vm.startPrank(BORROWER);\n        vault.lock(1000 ether);\n        vault.draw(1 ether);\n        vm.stopPrank();\n\n        vm.warp(t0 + 59 minutes);\n        vm.roll(block.number + 295);\n        spot.seed(V * 8 / 10); // inside the live epoch: anchor V, allowance 20%\n\n        vm.warp(t0 + 61 minutes);\n        vm.roll(block.number + 10);\n        primary.seed(V);\n        spot.seed(V); // committed code: ExcessDeviation, the next epoch anchored at 0.8 V\n        (uint256 value,) = spot.latestValue();\n        assertEq(value, V, \"the honest spot lands once the push's epoch has expired\");\n        vm.prank(BORROWER);\n        vault.draw(1 ether); // and the vault's price actions resume at agreeing prices\n    }\n}","reproduction":"test/scratch/SpotEpochLockoutProof.t.sol (fails on this code with ExcessDeviation; passes once a value within the cap of the expired epoch's anchor is accepted). Plain CDPVault over MockIMD with the shipped PriceFeed, NhiFeed and SpotFeed artifacts (maxAge 1h/1d/1h, cap 2000) seeded through `_accept` (the signature path is the attester's; `_checkValue`/`_accept` are exactly what submitAttestation runs). t0: nhi 0.9e18, primary V=4e15, spot V; borrower locks 1000 IMD and draws 1 imdUSD (works). t0+59min: spot.seed(0.8V) is ACCEPTED (inside the live epoch anchored at V). t0+61min: primary.seed(V) lands; spot.seed(V) EXPECTED to land (it is 0% from the previous anchor V), ACTUAL `ExcessDeviation` (the new epoch anchored at 0.8V, allowance 20%, V is +25%). Then vault.draw(1e18) EXPECTED to succeed, ACTUAL reverts `PriceDivergence` (test/scratch/SpotEpochLockout.t.sol demonstrates the full two hours: spot.seed(V) still reverts at t0+59min+2h-1s, draw reverts `StaleFeed` by then, and the honest V lands only at t0+59min+2h).","severity":"medium","snippet":"        return (_value, _allowanceNow());","title":"SwarmFeed: a value relayed in the last minute of a live epoch becomes the next epoch's anchor, so one end-of-block pool push attested into the spot feed locks the honest spot out for two hours and hal"},{"citation":"resolved","description":"Question 8 (comment claims). The contract docstring (lines 34-39) and `_allowanceNow` (lines 425-428) say a re-anchor far from the market costs the attacker the hours of silence that widen the allowance, during which an honest refresh can land first. That holds for a STALE re-anchor. It does not hold for the fresh one: a value accepted in the last block of a live epoch is within the cap of that epoch's anchor, needs no silence, and is the anchor of the next epoch (`_epoch` line 416, `_accept` lines 477-479), so an honest value more than the cap away from it is refused for the two hours until the stale base is earned; nobody can refresh over it. The same premise is used by DeploymentConfig.sol lines 243-250 (WIDE_ALLOWANCE_BPS) and by the accepted bite bound at CDPVault.sol lines 1349-1352 ('the cost of the hold'). The mechanism and the fix are the medium finding against line 416; this entry records the three places whose stated reason depends on it. Separately, two DeploymentConfig comments describe Sepolia values the mainnet plan rewrites without touching the comment: line 4 ('named in the approved Sepolia workflow (miyagod.eth)') and line 19 ('Chainlink ETH/USD on Sepolia'); after `plan.py --write` the constants below them are mainnet's while the sentences still say Sepolia.","line":39,"path":"src/SwarmFeed.sol","reproduction":"test/scratch/SpotEpochLockout.t.sol, test_onePushedSpotAtTheEpochEndLocksTheHonestSpotOutForTwoHours (passes on this code: it demonstrates the state). Spot seeded V at t0 (live epoch, anchor V); seeded 0.8V at t0+59min (accepted, no silence); at t0+61min and again at t0+59min+2h-1s the honest V reverts ExcessDeviation; it lands only at t0+59min+2h. Expected from the docstring: the honest refresh lands as soon as the epoch that admitted the 0.8V value has expired. Actual: refused for two hours.","severity":"info","snippet":"/// re-anchor cost an attacker those same hours of silence, during which anyone can refresh the feed.","title":"SwarmFeed NatSpec claims a far re-anchor costs the attacker hours of silence 'during which anyone can refresh the feed'; a value landed at the end of a live epoch re-anchors with no silence and cannot"}],"hash":"0d0d77da090edd2f73967ce34f53fdac7c7b05df0b620fc9882880decbf49641","nodeId":"b82bc34f-2326-4324-ad31-16eacdef9e93","outcome":"completed","summary":"The review is complete and the findings file is written. Summary of what I found and what I checked:\n\n**Findings written to `.imd-findings.json`** (2 entries):\n\n- **Medium, `src/SwarmFeed.sol:416`.** A value relayed in the last minute of a live epoch becomes the next epoch's anchor with the fresh 20% allowance, and nothing lets an honest reading undo it. For the spot feed, whose recipe reads one block, that is one end-of-block pool push of about 20%, restored the next block (about $5k in fees, no hold to defend against arbitrage), attested honestly and relayed at the epoch's end. The honest spot is then 25% above the anchor and refused for two hours, during which every price action reverts (`PriceDivergence` for an hour, then `StaleFeed`): draw, priced free, cash, bark, heel, bite, resecure. Marks whose bite window falls inside expire and need a fresh six-hour grace, and redemptions close. It is repeatable at about $5k per three hours. It also undercuts the \"cost of the hold\" reason given for the accepted held-down-pool liquidation bound, if the primary's sample blocks are predictable. Proof: `test/scratch/SpotEpochLockoutProof.t.sol` fails on this code with `ExcessDeviation` and passes once a new epoch also accepts a value within the cap of the expired epoch's anchor and anchors there. Smallest fix stated in the entry.\n- **Info, `src/SwarmFeed.sol:39`.** The docstring, the WIDE_ALLOWANCE comment and the bite bound all rest on \"a far re-anchor costs hours of silence during which anyone can refresh\", which the above shows is false for a late-epoch value. Two DeploymentConfig comments still say Sepolia after plan.py rewrites the values.\n\n**Answers where nothing is wrong.** Attestation checks (signer, domain, question splice, span, advancing and recent window, replay, panel floors), the stale-growth allowance arithmetic, Chainlink decoding and 2h staleness, the asker's triggers, budget cap, back-off and non-reverting callback, and relay bundling all match their NatSpec. Borrowing and liquidation: every ordering I traced leaves debt backed, seizures at `1.2/price`, dust and remainder sweeps, bad-debt recording and `cover` behave as bounded. Redemption: payout is capped at par and paid at `max(attested, paced)`; the supply identity holds with fees minted. The paced debt errs low in every sequence I replayed, including the wipe-and-redraw and cancel-then-draw cases. Treasury: every exit is guarded and state is written before transfers; `unwrapForOracle` is correctly unguarded. Governance: every bound and the 48h delay hold; `line` has no upper bound, which is a stated trust assumption. Deployment: initcode margin is 1,278 bytes and runtime margin 978; body windows fit the span policies; stage ordering and `verifySeeded` are as the runbook says. Reentrancy: all state-changing entry points are guarded or no-ops on re-entry.\n\n**Read in full:** every file in `src/` and `src/interfaces/`, `script/DeployMainnet.s.sol`, `script/DeployPreflight.sol`, `deploy/mainnet/` (plan.py, check-bodies.mjs, rehearse-fork.sh, the three bodies), runbook sections 6 and 7, PARAMETERS-2026-10-05.md, the diff since e4baedf. **Not reachable:** StakedIMD and IMD source on mainnet (the no-hook and withdraw-only-hold premises), the Intake, the oracle service's sample-block formula, the keeper, and the live Chainlink aggregator. The full existing suite did not finish inside my window; the two scratch tests ran as reported.","treeHash":null,"usage":{"cachedInputTokens":6322919,"inputTokens":802,"model":"claude-fable-5-1","outputTokens":116694,"runtime":"claude","turns":61,"wallClockMs":1843960}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"74a99f640688d37b","findings":[{"citation":"resolved","description":"ATTESTATION_RELAYER (0xe36F...) and CHAINLINK_ETH_USD (0x694A..., Sepolia's aggregator) are compiled into PriceFeed, NhiFeed, SpotFeed, SwarmWorkOracle and UsdPriceFeed as immutable authorities. At c7d50ee `forge script script/DeployMainnet.s.sol --sig check()` plans SwarmRelay at 0x9AEb55c7A16C11B37DC96BD22a33906D1668e20B and prints `CONFIG ATTESTATION_RELAYER ... CHANGE`, `CONFIG CHAINLINK_ETH_USD 0x5f4eC3Df... CHANGE`, plus CHANGE for WORK_ORACLE_FACTORY, ORACLE_ASKER and TREASURY_FACTORY. The task names the last four placeholders as not-findings; ATTESTATION_RELAYER and CHAINLINK_ETH_USD are not placeholders but are equally wrong for mainnet. `_refuseUnlessReady` does refuse the broadcast while any disagrees, so nothing can be deployed wrong from this commit, but it also means this commit cannot be the deploy commit: deploy/mainnet/plan.py --write must rewrite DeploymentConfig (and APPROVED_OPERATOR / FEE_RECIPIENT / INTAKE) first, and every address-bearing bytecode (the three feeds, the asker, the Treasury, the vault) changes with it. The review of 'the commit that will deploy' therefore has to be re-run on the converged commit; what was audited here is the logic, not the bytes that ship.","line":77,"path":"src/DeploymentConfig.sol","reproduction":"Run `forge script script/DeployMainnet.s.sol --sig \"check()\"` at c7d50ee. Expected (a deploy-ready commit): five lines ending ` ok`. Actual: `CONFIG ATTESTATION_RELAYER 0x9AEb55c7A16C11B37DC96BD22a33906D1668e20B CHANGE`, `CONFIG WORK_ORACLE_FACTORY 0x7A8D... CHANGE`, `CONFIG ORACLE_ASKER 0x02a7... CHANGE`, `CONFIG TREASURY_FACTORY 0xeCa9... CHANGE`, `CONFIG CHAINLINK_ETH_USD 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 CHANGE`. Smallest fix: run plan.py --write with the cold operator and the Intake, commit, and re-pin the review to that commit; the NatSpec on line 19 ('Chainlink ETH/USD on Sepolia') should then read mainnet.","severity":"info","snippet":"address constant ATTESTATION_RELAYER = 0xe36FFc2688Bf5974f2187AC9086492e372926D40;","title":"Pinned authority constants at this commit are not the planned mainnet addresses (check() reports CHANGE for all five); the commit that deploys will differ from the one reviewed"},{"citation":"resolved","description":"Parameters.sol:142 and DeploymentConfig.sol:144 (`earnLine = reserveValueUsd + totalDebt * EARN_MAT_BPS / 10000`) describe the work ceiling's ratio term as a share of totalDebt. ParameterizedVault.earnLine (line 279-281) computes `reserveValue() + mulDiv(backedDebt(), parameters.earnMat(), 10_000)` and backedDebt (line 262-270) is `min(totalDebt, debtAtTransactionStart, pacedDebtNow) - totalBadDebt`. The difference is the whole point of the D1 fix: with a fresh $1M book the paced debt is ~10,000 imdUSD after one hour, so earnLine is 2,500 imdUSD, not 250,000. A reader of Parameters or DeploymentConfig sizing a wage proposal, or an auditor checking the work ceiling against the stated formula, gets a figure up to two orders of magnitude too high during the first day after any large draw. Code is right; the two comments are wrong.","line":142,"path":"src/Parameters.sol","reproduction":"State: ParameterizedVault with one position that drew 1,000,000 imdUSD one block ago, empty register, EARN_MAT_BPS 2500. Expected from the NatSpec: earnLine() = 0 + 1,000,000 * 0.25 = 250,000e18. Actual: backedDebt() = min(1e24, 1e24, _pacedDebt) where _pacedDebt = min(live, 0 + 10% of max(0, 100,000e18) * elapsed/1h) = 10,000e18 after one paced hour, so earnLine() = 2,500e18. Fix: change both comments to 'of backedDebt (totalDebt capped at the transaction's opening debt and the paced debt, less totalBadDebt)'.","severity":"low","snippet":"    /// @notice The live ratio term of the vault's work ceiling, in basis points of totalDebt.","title":"NatSpec claims earnMat is 'in basis points of totalDebt' and earnLine = reserveValueUsd + totalDebt * EARN_MAT_BPS / 10000; the code uses backedDebt (paced, capped at the transaction's opening debt, l"},{"citation":"resolved","description":"CDPVault.bite line 1380 states the seizure never exceeds floor(debtToRepay * 1.2e18 / price). Lines 1403-1407 then fold in `remainder` (the collateral left after the seizure, when it is under _oneWeiSeizure(price) and debt survives), so the transfer is formula + remainder. The function NatSpec at 1332-1334 also says 'Collateral must cover the full payout, except dust below the seizure for one wei of debt, which is taken whole' without mentioning the second sweep. The sweep is correct and deliberate (it is what makes _recordBadDebt reachable), but a reader or an integrator computing the liquidator's receipt from the stated formula, or the borrower's loss from the NatSpec, is off by the dust, and the comment at 1380 is simply false as written. Doc-only; no funds at risk.","line":1380,"path":"src/CDPVault.sol","reproduction":"State: position with debt 2e18 and collateral exactly 1.2e18*1e18/price + k raw units where 0 < k < _oneWeiSeizure(price); call bite(owner, 1e18). Expected per line 1380: collateralSeized = floor(1e18 * 1.2e18 / price). Actual: collateralSeized = that + k (lines 1403-1407), emitted in Bite.collateralSeized. Fix: reword line 1380 to 'a bite never seizes more than the formula plus a remainder too small for any later bite (below)', and add the same clause to the NatSpec at 1332-1334.","severity":"low","snippet":"            // Any larger shortfall is still refused: a bite never seizes more than the formula.","title":"bite comment 'a bite never seizes more than the formula' contradicts the remainder sweep twenty lines below, which adds dust above the formula's payout"},{"citation":"resolved","description":"Lines 4-7 say the operator 'completes the two one-time links and operates the mock faucets'. On the mainnet path (DeployMainnet._vaultInit: stablecoin_ = 0, oracle_ = WORK_ORACLE_SENTINEL) the vault creates a bound ImdUSD (ImdUSD constructor branch vault_ == msg.sender, so _initializer stays zero and setVault is permanently AlreadyInitialized) and a SwarmWorkOracle with no grantRights, so there are no links to complete and no faucets. On mainnet APPROVED_OPERATOR is instead the Parameters governor (Governed.onlyGovernor), the Treasury withdrawer (Treasury.withdraw/withdrawNative/handOffLaunchFees) and the ImdUSD initializer only in the unused ImdUSD(address(0)) mode. Whoever reads this constant to understand what the cold key can do is pointed at powers it does not have and not at the ones it has.","line":4,"path":"src/DeploymentConfig.sol","reproduction":"Read the constant's powers from code: grep APPROVED_OPERATOR src/ gives Governed.sol:43 (propose/cancel), Treasury.sol:381,465,492 (withdraw, withdrawNative, handOffLaunchFees), ImdUSD.sol:31 (initializer only when constructed with address(0), which DeployMainnet never does). Expected: the NatSpec lists those. Actual: it lists 'two one-time links' and 'mock faucets', neither reachable on the mainnet vault (ImdUSD.setVault reverts AlreadyInitialized; SwarmWorkOracle has no grantRights). Fix: rewrite lines 4-7 to name the governor, withdrawer and launch-fee hand-off roles, and move the Sepolia note to history.","severity":"info","snippet":"/// @dev Explicit operator named in the approved Sepolia workflow (miyagod.eth).","title":"APPROVED_OPERATOR NatSpec describes the Sepolia workflow (two one-time links, mock faucets) that the mainnet deployment does not have"}],"hash":"0db00ed943bb2c4a0fa3d2409b6ed4054df9f17f57d7d3ac4bf224ce6692e87d","nodeId":"86d1f337-a0d3-430a-bce1-2427b7a54a2d","outcome":"completed","summary":"The scratch test confirms the figures in the earnLine finding exactly (backedDebt 10,000 imdUSD and earnLine 2,500 imdUSD one paced hour after a 1,000,000 draw, against the NatSpec's 250,000). The findings file is complete and valid. Final report follows.\n\n## Outcome\n\nThe protocol at c7d50ee holds up. I found no reachable way, with the committed constants, to take funds, free collateral a position needs, stop a liquidation that should happen, or overpay a redeemer beyond the bounds the code states. The four items in `.imd-findings.json` are one info about the commit itself and three documentation defects; none carries a proof because none is critical or high. All 628 unit tests pass at this commit.\n\n## Answers by question\n\n**1. Oracle.** Signer, domain, question splice, span, advancing and recent window, issuedAt ordering, replay and panel floors are all enforced in `submitAttestation` and `_requireQuestion`. The epoch anchor, the stale allowance earned only by whole hours of silence measured from the later of signature and relay, and the wide epoch closing behind its first value behave as documented. The first value is unbounded on chain and caught by `verifySeeded`. Chainlink's leg reads zero on any malformed answer and halts price actions within two hours. The asker's triggers, back-off and never-reverting callback spend at most the daily budget. Nothing lands a value the question does not support.\n\n**2. Borrowing and liquidation.** Every ordering I traced through lock, lockIMD, free, draw, wipe, bark, heel, bite and cover keeps debt backed or recorded as realized bad debt. The remainder sweep, the one-wei dust path and cover's re-lock pricing agree with their stated bounds. The held-down pool liquidation is exactly the ACCEPTED bound stated in `bite`.\n\n**3. Redemption and paced figures.** The payout is min(live, paced backing) times (1 minus fee) at the higher of attested and paced price, read before the burn, with same-transaction capital excluded. The reserve and candidate split reconciles to the wei. The paced debt errs low as stated; the fee base floor and ratchet hold.\n\n**4. Treasury.** Every exit is bounded as documented and now guarded. Bad debt gates imdUSD withdrawals and the stream. Nothing an outsider can take; `sync` on a junk address only mis-records that address.\n\n**5. Governance and work minting.** Every proposal kind is validated at proposal and application against its constant bound. `line` has no upper bound by design, which is a trust assumption, not a bypass. Wage zero refuses `earn` and `claim`.\n\n**6. Deployment.** `run` refuses to broadcast at this commit because five constants are not converged. That is the info finding: this commit cannot be the deploy commit, so the review must be re-pinned after `plan.py --write`.\n\n**7. Reentrancy.** Every external call writes state first or is guarded; sIMD, IMD and imdUSD have no hooks.\n\n**8. Comments.** Three contradictions found and filed: earnMat described as a share of totalDebt in two files, the `bite` comment \"never seizes more than the formula\", and the stale Sepolia operator NatSpec.\n\n**Read in full:** every file in src/ and src/interfaces/, DeployMainnet.s.sol, DeployPreflight.sol, plan.py, the three body templates, runbook sections 7, 7b and 8. **Not reached:** the sIMD and Intake source (external), check-bodies.mjs, rehearse-fork.sh, docs/PARAMETERS beyond its walk-cost section, and the fork and invariant suites, which I did not run.","treeHash":null,"usage":{"cachedInputTokens":3281926,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":59569,"runtime":"claude","turns":33,"wallClockMs":945219}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8af9903f4ad1eed0","findings":[{"citation":"resolved","description":"Oracle (question 1, 'holds a feed off'; question 2, 'stops a liquidation that should happen'). `_epoch()` anchors every new epoch at `_value`, whatever was accepted last, with the fresh allowance (maxDeviationBps, 20%) unless that value has been stale a whole STALE_GROWTH_PERIOD past maxAge (`_allowanceNow`, lines 429-444; `_accept`, lines 477-479). The epoch bound stops a WALK, but nothing lets an honest value UNDO a move made just before an epoch expired: a value accepted at t0+59min inside an epoch anchored at V (so up to 20% off V) becomes the anchor of the epoch that opens on the next acceptance after t0+60min, and the honest V, 25% above a 0.8V value, is refused `ExcessDeviation` until t0+59min+2h (the stale base is earned only by a whole hour of silence past the lifetime). For the spot feed this costs ONE block of the pool: the spot recipe reads the window's last block alone (SpotFeed.sol lines 46-48, samples 1), the buyer chooses toBlock (any block within maxAge/12 of head, span 150..1200), and the panel attests a pushed end-of-block state honestly. Sequence from an external caller: (1) read `epoch()` to learn when the live spot epoch opened (t0); (2) at about t0+50min sell about 24k IMD into the v4 pool as the last transaction of a block (a ~20% fall) and buy it back at the top of the next block (about $5k round trip in pool fees at launch depth, plus whatever arbitrage lands between the two bundles); (3) buy a spot attestation with toBlock at the pushed block and relay it through SwarmRelay at t0+59min (within the live epoch's 20% of V, so accepted); (4) from t0+60min every honest spot reading is refused for two hours. With the primary at V and spot at 0.8V, CDPVault._requirePriceAgreement reverts `PriceDivergence` (SKEW_BPS 500) for the first hour, then `StaleFeed` once the pushed spot ages past SPOT_MAX_AGE, so `draw`, priced `free`, `cash`, `barkFor`, `heel`, `bite`, `resecure` and a finite-ceiling `earn` all stop. A mark whose one-hour bite window (`tail()`) falls inside the halt expires (`_expired`) and must be retaken with a fresh six-hour grace, so a marked borrower can hold off their own liquidation for the cost of the push per seven hours; redemptions, the peg's defence, are closed for two hours. The Treasury does not pay to undo it (the spot feed is not keep-alive in DeployMainnet's asker policy, and the restored pool reads as a RISE against the 0.8V value, which DRIFT_RISE_TRIGGER_OF_CAP_BPS = 0 never buys), so the halt is not self-correcting; a borrower's askPaid for the spot during the lockout buys an honest answer the feed refuses, for the caller's 0.5 IMD. Repeatable: when the honest V lands at t0+59min+2h it opens a wide epoch anchored at 0.8V with `_epochFirst` = V, and the same push at that epoch's end re-anchors the next at 0.8V again. Reachable with the constants as committed (maxAge 1h, cap 2000, SKEW_BPS 500, grace 6h, tail 1h). The same premise is stated as a property in three places that the code does not have: SwarmFeed.sol lines 38-39 ('a far re-anchor cost an attacker those same hours of silence, during which anyone can refresh the feed') and 425-428, DeploymentConfig.sol lines 243-250 (WIDE_ALLOWANCE_BPS), and the accepted bite bound at CDPVault.sol lines 1349-1352 ('the cost of the hold ... for seven hours every arbitrageur ... must be absorbed'): a value pushed in through the end of a live epoch needs no silence and no hold, and nobody can refresh over it for two hours. Not previously recorded: docs/AUDIT-SWEEP-PANEL-ORACLE-2026-10-07.md item 6 covers two steps straddling a boundary in the WALK direction only; no earlier round states the lockout of the honest value, and PARAMETERS-2026-10-05.md lines 199-200 repeat the silence premise. Smallest fix: in `_checkValue`/`_accept`, when the stored epoch has expired, also accept a value within maxDeviationBps of the EXPIRED epoch's anchor (`_anchorValue`, or `_value` for a first epoch) and anchor the new epoch at that anchor in that case, so a","line":416,"path":"src/SwarmFeed.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// A value relayed in the last minute of a live SwarmFeed epoch becomes the next epoch's anchor. For the spot\n// feed, whose recipe reads one block, that is one end-of-block push of the pool (restored the next block),\n// attested honestly: the honest spot, 25% above a 0.8x push, is then refused ExcessDeviation until the pushed\n// value has been stale a whole hour (two hours after the push), and the vault refuses every price action\n// (PriceDivergence for the first hour, StaleFeed for the second). This test FAILS on the committed code at\n// the honest re-seed and passes once a value within maxDeviationBps of the expired epoch's anchor is accepted\n// when a new epoch opens (and anchors the new epoch there).\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {CDPVault} from \"src/CDPVault.sol\";\nimport {SwarmFeed} from \"src/SwarmFeed.sol\";\nimport {PriceFeed} from \"src/PriceFeed.sol\";\nimport {NhiFeed} from \"src/NhiFeed.sol\";\nimport {SpotFeed} from \"src/SpotFeed.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {APPROVED_OPERATOR} from \"src/DeploymentConfig.sol\";\n\ncontract LockoutProofPriceFeed is PriceFeed {\n    constructor() PriceFeed(1 hours, 2000) {}\n\n    function seed(uint256 v) external {\n        _accept(v, uint64(block.timestamp));\n    }\n}\n\ncontract LockoutProofNhiFeed is NhiFeed {\n    constructor() NhiFeed(1 days, 2000) {}\n\n    function seed(uint256 v) external {\n        _accept(v, uint64(block.timestamp));\n    }\n}\n\ncontract LockoutProofSpotFeed is SpotFeed {\n    constructor() SpotFeed(1 hours, 2000) {}\n\n    function seed(uint256 v) external {\n        _accept(v, uint64(block.timestamp));\n    }\n}\n\ncontract SpotEpochLockoutProofTest is Test {\n    address private constant BORROWER = address(0xB0B);\n    uint256 private constant V = 4e15; // wei of ETH per 1e18 IMD: the honest spot and primary\n\n    MockIMD private imd;\n    CDPVault private vault;\n    LockoutProofPriceFeed private primary;\n    LockoutProofNhiFeed private health;\n    LockoutProofSpotFeed private spot;\n\n    function setUp() public {\n        vm.warp(1_000_000);\n        vm.roll(20_000_000);\n        imd = new MockIMD();\n        primary = new LockoutProofPriceFeed();\n        health = new LockoutProofNhiFeed();\n        spot = new LockoutProofSpotFeed();\n        vault = new CDPVault(address(imd), address(0), address(0), address(primary), address(health), address(spot));\n        vm.prank(APPROVED_OPERATOR);\n        imd.mint(BORROWER, 1_000_000 ether);\n        vm.prank(BORROWER);\n        imd.approve(address(vault), type(uint256).max);\n    }\n\n    /// @dev t0: an honest refresh (anyone's 0.5 IMD) opens a live spot epoch anchored at V. t0 + 59 min: the\n    /// attacker relays a spot attestation of 0.8 V, an honest reading of the one block the pool was pushed in\n    /// (inside the epoch's 20% of V, so accepted). t0 + 61 min: the epoch has expired; the honest V is within\n    /// 20% of the expired epoch's anchor and must land, so the vault's price actions resume. On the committed\n    /// code the new epoch anchors at 0.8 V with the fresh 20% allowance, V is 25% above it, and this reverts\n    /// ExcessDeviation; the vault then refuses draw, bark, bite and cash for two hours.\n    function test_honestSpotLandsOnceTheLatePushsEpochHasExpired() public {\n        health.seed(0.9e18);\n        primary.seed(V);\n        spot.seed(V);\n        uint256 t0 = block.timestamp;\n        vm.startPrank(BORROWER);\n        vault.lock(1000 ether);\n        vault.draw(1 ether);\n        vm.stopPrank();\n\n        vm.warp(t0 + 59 minutes);\n        vm.roll(block.number + 295);\n        spot.seed(V * 8 / 10); // inside the live epoch: anchor V, allowance 20%\n\n        vm.warp(t0 + 61 minutes);\n        vm.roll(block.number + 10);\n        primary.seed(V);\n        spot.seed(V); // committed code: ExcessDeviation, the next epoch anchored at 0.8 V\n        (uint256 value,) = spot.latestValue();\n        assertEq(value, V, \"the honest spot lands once the push's epoch has expired\");\n        vm.prank(BORROWER);\n        vault.draw(1 ether); // and the vault's price actions resume at agreeing prices\n    }\n}","reproduction":"test/scratch/Proof_73542bc9e37c.t.sol (run: forge test --match-path test/scratch/Proof_73542bc9e37c.t.sol). Plain CDPVault over MockIMD with the shipped PriceFeed, NhiFeed and SpotFeed artifacts (maxAge 1h/1d/1h, cap 2000) seeded through `_accept` (the signature path is the attester's; `_checkValue`/`_accept` are exactly what submitAttestation runs). t0: nhi 0.9e18, primary V=4e15, spot V; borrower locks 1000 IMD and draws 1 imdUSD (works). t0+59min: spot.seed(0.8V) is ACCEPTED (inside the live epoch anchored at V, allowance 20%). t0+61min: primary.seed(V) lands; spot.seed(V) EXPECTED to land (0% from the previous anchor V), ACTUAL reverts `ExcessDeviation` (the new epoch anchored at 0.8V with allowance 20%; V is +25%). Verified by running it: '[FAIL: ExcessDeviation()] test_honestSpotLandsOnceTheLatePushsEpochHasExpired'. With spot stuck at 0.8V and the primary at V, vault.draw(1e18) reverts PriceDivergence (SKEW 500) until the pushed spot is stale, then StaleFeed; by `_allowanceNow` the honest V is accepted only at t0+59min+2h, when the 0.8V value has been stale a whole hour.","severity":"medium","snippet":"        return (_value, _allowanceNow());","title":"SwarmFeed: a value relayed at the end of a live epoch anchors the next one, so one end-of-block spot push attested honestly refuses the honest spot for two hours and halts every price action in the va"},{"citation":"resolved","description":"Question 8 (comment claims). Parameters.sol:142 and DeploymentConfig.sol:144 (`earnLine = reserveValueUsd + totalDebt * EARN_MAT_BPS / 10000`) describe the work ceiling's ratio term as a share of totalDebt. ParameterizedVault.earnLine (lines 279-281) computes `reserveValue() + mulDiv(backedDebt(), parameters.earnMat(), 10_000)`, and backedDebt (lines 262-270) is `min(totalDebt, debtAtTransactionStart, pacedDebtNow) - totalBadDebt`. The difference is the whole point of the D1 fix: with a fresh $1M book the paced debt is about 10,000 imdUSD after one paced hour (FOLLOW 10% an hour of the 100,000 floor), so earnLine is 2,500 imdUSD, not 250,000. A governor sizing a wage proposal from Parameters, or a reader checking the work ceiling against the stated formula, gets a figure up to two orders of magnitude too high in the day after any large draw. The code is right; both comments are wrong. Doc-only, no funds at risk; the wage is 0 at launch. Fix: change both comments to 'of backedDebt (totalDebt capped at the transaction's opening debt and the paced debt, less totalBadDebt)'.","line":142,"path":"src/Parameters.sol","reproduction":"Read ParameterizedVault.sol:279-281 and 262-270 against Parameters.sol:142 and DeploymentConfig.sol:144. State: ParameterizedVault with one position that drew 1,000,000 imdUSD one block ago, empty register, EARN_MAT_BPS 2500, one paced hour elapsed. Expected from the NatSpec: earnLine() = 0 + 1,000,000 * 0.25 = 250,000e18. Actual: backedDebt() = min(1e24, 1e24, _pacedDebtNow()) where the paced debt has followed at most 10% of max(live, 100,000e18 floor) per paced hour, about 10,000e18, so earnLine() = 2,500e18.","severity":"low","snippet":"    /// @notice The live ratio term of the vault's work ceiling, in basis points of totalDebt.","title":"Parameters and DeploymentConfig NatSpec state earnLine's ratio term as a share of totalDebt; the code uses backedDebt (capped at the transaction's opening debt and the paced debt, less totalBadDebt)"},{"citation":"resolved","description":"Question 8. CDPVault.bite line 1380 states the seizure never exceeds floor(debtToRepay * 1.2e18 / price). Lines 1403-1407 then fold in `remainder` (the collateral left after the seizure when it is below _oneWeiSeizure(price) and debt survives), so the transfer is formula + remainder. The function NatSpec at 1332-1334 ('Collateral must cover the full payout, except dust below the seizure for one wei of debt, which is taken whole') also omits the sweep. The sweep is deliberate and correct (it is what makes _recordBadDebt reachable), but an integrator computing the liquidator's receipt from line 1380, or the borrower's loss from the NatSpec, is off by the dust, and the sentence at 1380 is false as written. Doc-only. Fix: reword 1380 to 'a bite never seizes more than the formula plus a remainder too small for any later bite (below)' and add the same clause at 1332-1334.","line":1380,"path":"src/CDPVault.sol","reproduction":"State: position with debt 2e18 and collateral exactly floor(1e18 * 1.2e18 / price) + k raw units, 0 < k < _oneWeiSeizure(price); fresh agreeing feeds; the position marked and past grace. Call bite(owner, 1e18). Expected per line 1380: collateralSeized = floor(1e18 * 1.2e18 / price). Actual (lines 1403-1407): collateralSeized = that + k, emitted as Bite.collateralSeized, since remainder = k != 0, debtToRepay < debt and k < _oneWeiSeizure(price). test/scratch/BiteDustJudge.t.sol's first bite shows the same mechanism: the largest formula seizure leaves collateral 0 (the raw remainder swept) and the position drained.","severity":"low","snippet":"            // Any larger shortfall is still refused: a bite never seizes more than the formula.","title":"bite comment 'a bite never seizes more than the formula' and the function NatSpec omit the remainder sweep twenty lines below, which adds dust above the formula's payout"},{"citation":"resolved","description":"Question 2. When `collateralSeized > position.collateral` and the collateral is below `_oneWeiSeizure(price)`, bite seizes the whole remainder instead of reverting (lines 1375-1385). The branch exists so one wei of debt can clear dust no formula seizure reaches, but it does not bound `debtToRepay`: any amount up to the position's full accrued debt passes the ExcessRepayment check at 1373, is burned from the caller by `_payDebt`, and is paid with the dust. The bad debt is then retired by the liquidator rather than by `cover` (the Treasury) and totalBadDebt falls. Nobody else can take anything (the loss is the caller's own imdUSD, and the protocol gains), so this is a keeper footgun, not a theft: a keeper that computes debtToRepay from positions(owner).debt for a drained-then-relocked position burns its inventory for one raw unit. Reachable with the constants as committed. Smallest fix: in the dust branch require `debtToRepay == 1` (or at most the dust's value in debt plus one wei) before `collateralSeized = position.collateral;`, so the remainder of the debt stays on the `cover` path.","line":1384,"path":"src/CDPVault.sol","reproduction":"test/scratch/BiteDustJudge.t.sol test_dustBranchAcceptsFullDebtForOneRawUnit (PASSES on this code: it demonstrates the state). Base CDPVault over MockIMD, price feeds 1e18, NHI 0.9. Borrower locks 1,700 IMD, draws 1,000 imdUSD; keeper locks 5,000, draws 2,000. Price steps 1 -> 0.8 -> 0.64 -> 0.512 an hour apart; keeper barks; after lull()+ the keeper bites the largest coverable debt (1700e18*0.512e18/1.2e18): collateral 0, debt 274.713e18 recorded as totalBadDebt. Borrower calls lock(1). Keeper calls bite(borrower, 274713043378995433667), the full debt. Expected: refused, or bounded to the one wei the branch is written for, with the rest left to cover. Actual: succeeds; keeper burns 274.713 imdUSD and receives 1 raw unit; position debt 0; totalBadDebt 0 (logged by the test).","severity":"info","snippet":"            collateralSeized = position.collateral;","title":"bite's dust branch accepts any debtToRepay, so a keeper repaying the full debt against sub-one-wei dust burns its whole repayment for one raw unit"},{"citation":"resolved","description":"Question 8. `lock`, `lockIMD` and `wipe` clear a mark through `_clearIfRecovered` (lines 1729-1740), which requires `priced != 0`, `_priceAgrees()` (fresh primary, NHI, collateral price and spot, spot within skew of the primary) and health at that price. While any feed is stale or the two price feeds diverge, a marked borrower who tops up or repays above mat keeps the mark; because grace has already elapsed it is actionable the moment feeds are fresh again if the price is then below recovery, with no new grace. `_clearIfRecovered`'s own NatSpec states the condition; the heel summary at 1319-1320 and the bite NatSpec's defence line at 1349 ('a marked borrower who tops up or repays above mat clears the mark') do not. Doc-only: state the fresh-and-agreeing condition in both places, or tell a marked borrower to call `heel` once feeds are fresh.","line":1320,"path":"src/CDPVault.sol","reproduction":"State: a position marked underwater; spot feed stale (SPOT_MAX_AGE one hour, bought on demand); primary recovers. Borrower calls lock(amount) bringing the ratio above mat. Expected per line 1320: the deposit clears the mark. Actual: `_clearIfRecovered` returns without clearing because `_priceAgrees()` is false (spot stale); `liquidationMarks[owner].marked` stays true, and once spot is refreshed at a price below recovery `bite` is open at once (grace already elapsed, within tail).","severity":"info","snippet":"    /// when recovery is observed; deposit, repayment and successful borrowing/withdrawal also clear them.","title":"heel NatSpec and the bite defence line say deposit and repayment clear a mark; they clear it only on a recovery observed at fresh, agreeing feeds"},{"citation":"resolved","description":"Question 8. Lines 18-22 argue the raw staticcall keeps the work channel open: a dead or malformed aggregator reads as zero, reports stale, and 'values whatever it priced at nothing', so only the reserve term of earnLine degrades. That is true of the view earnLine(). It is not true of the channel: ParameterizedVault._pricingStale() (lines 226-228) is `super._pricingStale() || collateralPriceFeed.isStale()`, collateralPriceFeed is SharePriceFeed over this UsdPriceFeed, and CDPVault.earn calls _requireFreshFeeds() (line 576), so once the ETH/USD answer is older than ETH_USD_MAX_AGE, missing, non-positive or malformed, every earn reverts StaleFeed. The halt is the documented behaviour at ParameterizedVault.sol:220-225 ('a dead Chainlink ETH/USD leg stops minting, marking and liquidation here'); the UsdPriceFeed docstring predates the USD denomination. No funds at risk; halting is the safer direction. Fix: reword 18-22 to say the raw read keeps earnLine(), backingPerUnit() and reserveValueUsd() from REVERTING while the leg is down (views and the Treasury register stay readable, the reserve term reads zero), and that price-dependent actions including earn are refused by the vault's staleness check until Chainlink answers.","line":22,"path":"src/UsdPriceFeed.sol","reproduction":"State: ParameterizedVault at the shipped constants, wage nonzero, a holder of minting rights. Input: CHAINLINK_ETH_USD.latestRoundData() returns updatedAt = block.timestamp - 2 hours - 1 (or answer <= 0, or fewer than 160 bytes). Call vault.earn(1). Expected from the docstring: the channel stays open with the reserve term of earnLine at zero. Actual: UsdPriceFeed.isStale() true (line 52), SharePriceFeed.isStale() true, ParameterizedVault._pricingStale() true, CDPVault._requireFreshFeeds() reverts StaleFeed() at line 576 before the ceiling is read. earnLine() itself does not revert and reports reserveValue() == 0, the half of the claim that holds.","severity":"info","snippet":"/// it priced at nothing. Degrading the ceiling is the safe direction; bricking the channel is not.","title":"UsdPriceFeed docstring says a dead ETH/USD leg only degrades the work ceiling; on ParameterizedVault it halts earn through _requireFreshFeeds"},{"citation":"resolved","description":"Question 8, merged from audit_permissions (lines 4 and 77) and audit_flow (line 13). Lines 4-7 say APPROVED_OPERATOR 'completes the two one-time links and operates the mock faucets': on the mainnet path (DeployMainnet._vaultInit passes stablecoin_ = 0 and WORK_ORACLE_SENTINEL) the vault creates a bound ImdUSD in its constructor (ImdUSD.setVault is permanently AlreadyInitialized) and a SwarmWorkOracle with no grantRights, so there are no links and no faucet; what the key actually holds is the Parameters governor (Governed.onlyGovernor: propose/cancel), Treasury.withdraw, withdrawNative and handOffLaunchFees, and ImdUSD's initializer only in the ImdUSD(address(0)) mode the deployment never uses. Line 13 says FEE_RECIPIENT 'MUST NOT be the feed's reporter or relayer': there is no reporter (SwarmFeed.report was removed, as lines 79-83 of the same file say; DeployMainnet.verifyFeeds asserts report(uint256) is unreachable) and the relayer is the permissionless SwarmRelay, so the sentence names roles nobody holds. Line 19 says 'Chainlink ETH/USD on Sepolia' and line 4 'the approved Sepolia workflow (miyagod.eth)': deploy/mainnet/plan.py --write rewrites the constants beneath them (and the runbook's section 3 table lists CHAINLINK_ETH_USD and ATTESTATION_RELAYER as must-change, so their current Sepolia values are not themselves a defect) but touches no comment, so the release commit will carry mainnet addresses under sentences that still say Sepolia. OracleAsker.sol:52 quotes a paid update at '~$4.25'; 0.5 IMD at the parameters doc's $10.92 is about $5.46. Nothing on chain depends on any of these; they mislead a reader of the deploy commit about who holds what. Fix: rewrite lines 4-7 to name the governor, withdrawer and launch-fee hand-off roles; delete or reword line 13; drop 'Sepolia' from lines 4 and 19 (or have plan.py rewrite them); update the OracleAsker figure.","line":13,"path":"src/DeploymentConfig.sol","reproduction":"grep -n APPROVED_OPERATOR src/*.sol: Governed.sol (propose/cancel), Treasury.sol (withdraw, withdrawNative, handOffLaunchFees), ImdUSD.sol (initializer only when constructed with address(0), which DeployMainnet never does). grep -n 'function report' src/SwarmFeed.sol: no match. script/DeployMainnet.s.sol _vaultInit: stablecoin_ = 0, oracle_ = WORK_ORACLE_SENTINEL. deploy/mainnet/plan.py lines 59-66: set_constant on APPROVED_OPERATOR, FEE_RECIPIENT, INTAKE and the planned addresses only; no comment is rewritten. Expected: the comments describe the shipped roles and the chain. Actual: they describe the Sepolia release's. `forge script script/DeployMainnet.s.sol --sig 'check()'` at c7d50ee prints CHANGE for ATTESTATION_RELAYER and CHAINLINK_ETH_USD (0x5f4eC3Df...) alongside the three placeholders, as the runbook's section 3 says it must until plan.py --write runs.","severity":"info","snippet":"/// MUST NOT be the feed's reporter or relayer — whoever sets the price would otherwise profit from","title":"DeploymentConfig header comments describe the Sepolia release's roles (one-time links, mock faucets, a feed reporter, 'Chainlink ETH/USD on Sepolia') that the mainnet code does not have; OracleAsker q"}],"hash":"197c469eab1ddb97b7fc9fd219d7b866ac00ae39ca99a6439330f3ee1a77302d","nodeId":"700cb068-0683-4b7e-9b0c-b06e394252b8","outcome":"completed","summary":"Review complete. The findings file `.imd-findings.json` holds seven verified findings; the scratch tests that back them are in `test/scratch/`.\n\n**Kept, with reproductions run**\n\n- **Medium, SwarmFeed.sol:416.** A value relayed in the last minute of a live epoch anchors the next epoch with the fresh 20% allowance, so one end-of-block spot push attested honestly locks the honest spot out for two hours and the vault refuses every price action (PriceDivergence, then StaleFeed). A mark's one-hour bite window can be made to expire inside the halt, so a borrower can stall their own liquidation for the cost of one block's push per seven hours. The specialist's proof fails on this code with ExcessDeviation exactly as stated, and it is attached. The companion info finding about the \"hours of silence\" docstring is merged into it, with the three comment sites listed.\n- **Low, Parameters.sol:142 and DeploymentConfig.sol:144.** The ratio term is documented as a share of totalDebt; the code uses backedDebt, which can be two orders of magnitude lower in the day after a large draw.\n- **Low, CDPVault.sol:1380.** \"A bite never seizes more than the formula\" contradicts the remainder sweep at lines 1403 to 1407.\n- **Info, CDPVault.sol:1384.** The dust branch accepts any debtToRepay. My own test shows a keeper burning 274.7 imdUSD for one raw unit of collateral, with totalBadDebt falling to zero. Self-inflicted, so info.\n- **Info, CDPVault.sol:1320.** Deposit and repayment clear a mark only at fresh, agreeing feeds.\n- **Info, UsdPriceFeed.sol:22.** A dead ETH/USD leg halts earn through the vault's staleness check rather than only degrading the ceiling.\n- **Info, DeploymentConfig.sol:13.** The two specialists' stale-comment findings merged into one: one-time links and faucets, a reporter that no longer exists, \"Sepolia\" labels that plan.py does not rewrite, and the $4.25 figure in OracleAsker.\n\n**Dropped or folded**\n\n- The \"pinned constants are not mainnet\" finding. The runbook's section 3 table and plan.py document CHAINLINK_ETH_USD and ATTESTATION_RELAYER as must-change values rewritten before release, so it is process state, not a defect. I confirmed the check script prints CHANGE for all five and recorded that inside the comment finding.\n- One correction to the medium's description: the Treasury does not spend budget during the lockout, because the spot feed is not keep-alive and a restored pool reads as a rise, which is never bought. I rewrote that sentence.\n\n**Coverage.** I read SwarmFeed, SpotFeed, UsdPriceFeed, ParameterizedVault, DeploymentConfig, SwarmRelay and TransientReentrancyGuard in full, and CDPVault, Treasury, OracleAsker and Parameters in the parts the findings touch, plus the source diff since the previous sweep. I did not read ImdUSD, Governed, SwarmWorkOracle, the factories, PriceFeed, NhiFeed, SharePriceFeed, the deploy scripts, plan.py or runbook section 7 in full, so this judgement covers the specialists' claims rather than a fresh audit of those areas.","treeHash":null,"usage":{"cachedInputTokens":2167183,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":31313,"runtime":"claude","turns":46,"wallClockMs":460070}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"72f49cf84b9ab056","findings":[{"citation":"resolved","description":"The contract docstring argues that reading the Chainlink leg with a raw staticcall keeps the work channel open: a dead or malformed aggregator reads as zero, reports stale, and 'values whatever it priced at nothing', so only the reserve term of earnLine degrades. That is true of the view earnLine(). It is not true of the channel itself: ParameterizedVault._pricingStale() (src/ParameterizedVault.sol:226-228) is `super._pricingStale() || collateralPriceFeed.isStale()`, collateralPriceFeed is SharePriceFeed over this UsdPriceFeed, and CDPVault.earn calls _requireFreshFeeds() (src/CDPVault.sol:576), so once the ETH/USD answer is older than ETH_USD_MAX_AGE (2 hours), missing, non-positive or malformed, every earn reverts StaleFeed. The halt is the documented behaviour in ParameterizedVault.sol:220-225 ('a dead Chainlink ETH/USD leg stops minting, marking and liquidation here'); the UsdPriceFeed docstring predates the USD denomination and now states a property the shipped composition does not have. No funds at risk; the code's direction (halt rather than misprice) is the safer one. Fix: reword lines 18-22 to say the raw read keeps earnLine(), backingPerUnit() and reserveValueUsd() from REVERTING while the leg is down (so views and the Treasury register stay readable and the reserve term reads zero), and that price-dependent actions including earn are refused by the vault's staleness check until Chainlink answers again.","line":22,"path":"src/UsdPriceFeed.sol","reproduction":"State: a ParameterizedVault at the shipped constants, wage nonzero, a holder of minting rights. Input: CHAINLINK_ETH_USD.latestRoundData() returns updatedAt = block.timestamp - 2 hours - 1 (or answer <= 0, or fewer than 160 bytes). Call vault.earn(1). Expected from the UsdPriceFeed docstring: the channel stays open and the reserve term of earnLine reads zero. Actual: UsdPriceFeed.isStale() returns true, SharePriceFeed.isStale() returns true, ParameterizedVault._pricingStale() returns true, CDPVault._requireFreshFeeds() reverts StaleFeed() before the ceiling is read. The view earnLine() itself does not revert and reports reserveValue() == 0, which is the half of the claim that holds.","severity":"info","snippet":"/// it priced at nothing. Degrading the ceiling is the safe direction; bricking the channel is not.","title":"UsdPriceFeed NatSpec claims a dead ETH/USD leg only degrades the ceiling, but on ParameterizedVault it halts earn through _requireFreshFeeds"}],"hash":"4481e88ec4a67e4396919e8883fcff9d212736cb6bd1c93a61423a6010ed3166","nodeId":"cea0514c-58aa-4047-b55b-e3d4fd8b9111","outcome":"completed","summary":"The findings file is in place and parses. Here is the report.\n\n## Outcome\n\nNo arithmetic, rounding, boundary or ordering defect that lets anyone take or block value was found in the whole protocol at c7d50ee. One documentation discrepancy is recorded in `.imd-findings.json` at info severity. Every numeric claim in the comments, runbook section 7 and the parameter doc that I could recompute checks out, and the two paced bounds most exposed to abuse held under scratch tests I wrote and ran (then removed, as instructed).\n\n## The one finding\n\n**Info. UsdPriceFeed docstring overstates what the raw Chainlink read buys.** The docstring at `src/UsdPriceFeed.sol:18-22` says a dead ETH/USD leg \"degrades the ceiling\" rather than \"bricking the channel\". On the deployed vault, `earn` calls the staleness check, which includes the share price feed over this feed, so a Chainlink answer older than two hours, missing or malformed makes every `earn` revert with StaleFeed. The halt is the correct direction and is what ParameterizedVault's own NatSpec documents. Only the sentence in UsdPriceFeed is wrong. Reproduction and fix wording are in the findings file.\n\n## Answers by question\n\n**1. Oracle.** Attestation checks (relayer, chain id, panel floors, answer type, expiry, issue time monotone, replay nonce, EIP-712 digest with low-s and v in {27,28}, pinned question suffix, span bounds, advancing and recent window) are complete and ordered before any state write. The epoch arithmetic matches its comments exactly: stale allowance 40% after one whole hour past the lifetime, plus 2.5 points per further hour, 60% at ten silent hours for price feeds and 33 for NHI, capped at 1e6 bps; the straddle of one epoch boundary gives at most 1.44x per sliding hour, as documented. The first value is unbounded on chain, which the runbook's seeding check covers with the 5% band I recomputed from the deploy script. UsdPriceFeed and SharePriceFeed are decimal-agnostic per 1e18 raw units and degrade to zero or stale, never revert. The asker's triggers, in-flight slot, ten-minute interval, two-hour back-off written into `lastAsk`, and the daily top-up-to-budget are consistent; a refused Treasury purchase costs at most one price per two hours per feed. Pool inversion `1e18 * 2^192 / sqrtP^2` keeps about 15 significant digits at launch-scale prices.\n\n**2. Borrowing and liquidation.** `_collateralRatio` is exact: a fuzz over 1024 runs at prices below 1e16 per raw unit (where the whole-part term is zero) matched the 512-bit `collateral * price / (debt * 1e16)` on every run. `_mat` is monotone and stays in [170, 200]. The seizure `debt * 1.2e18 / price`, the bonus split from that seizure only, the one-wei-seizure dust rule and the remainder sweep are internally consistent, and the \"1.5x per 20% step, 9% bad debt after two steps\" figures in `bite`'s NatSpec recompute correctly. Fee accrual is linear from a checkpoint, `drip` runs before a rate change, and `chiOf` is set before principal exists.\n\n**3. Redemption and the paced figures.** Payout is `amount * min(live, paced backing) * (1 - fee) / max(attested, paced price)`, each term rounding against the redeemer; reserve-funded debt rounds up the cancelled figure. Scratch tests confirmed: a 20% attested fall half an hour after the last pacing paid at the paced price (100 IMD for 100 imdUSD, not 124), and a large draw against an under-backed book lifted `backingPerUnit` by nothing inside the hour and by no more than two points after it. The fee-base floor, divisor and cap arithmetic match the comments (9,000 imdUSD of burns to store the cap from the floor at divisor 2). The paced-debt claim \"errs low, never high\" holds for cancellations; the only path that keeps the figure unchanged is a wipe by one position and a draw by another inside one transaction, which the WIPED slot's NatSpec already states is per transaction, and the aggregate held debt is unchanged by it.\n\n**4. Treasury.** Each exit is guarded, writes its baseline befo","treeHash":null,"usage":{"cachedInputTokens":4419279,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":51125,"runtime":"claude","turns":37,"wallClockMs":689821}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"52c98c0dc01791cd","findings":[{"citation":"resolved","description":"In `bite`, when `collateralSeized > position.collateral` and the collateral is below `_oneWeiSeizure(price)`, the whole remainder is seized instead of reverting. That branch exists so one wei of debt can clear dust that no formula seizure can reach, but it does not bound `debtToRepay`: any amount up to the position's full accrued debt is accepted, burned from the caller, and paid with the dust. The position's bad debt is then retired by the liquidator rather than by `cover` (the Treasury), and `totalBadDebt` drops to zero. No outsider can take anything (the loss is the caller's own imdUSD), so this is a sharp edge for keepers rather than a theft; a keeper that computes `debtToRepay` from `positions(owner).debt` for a drained-then-relocked position burns its whole inventory for one raw unit. Smallest fix: in the dust branch require `debtToRepay == 1` (or at most the dust's value in debt plus one wei), e.g. `if (debtToRepay != 1) revert InsufficientCollateral();` before `collateralSeized = position.collateral;`, so the remainder of the debt stays on the `cover` path.","line":1384,"path":"src/CDPVault.sol","reproduction":"Base CDPVault, price feed 1e18, NHI 0.9. Borrower locks 1,700 IMD and draws 1,000 imdUSD; keeper locks 5,000 and draws 2,000. Price falls to 0.5e18; keeper barks, waits lull()+1. keeper calls bite(borrower, 708333333333333333333): seizes 1699999999999999999999 raw, the 1-raw remainder is swept (one-wei seizure at 0.5e18 is 2 raw), collateral 0, debt ~291.697e18 recorded as bad debt. Borrower calls lock(1). Keeper calls bite(borrower, 291697079033485539667) (the full debt). Expected: refused, or bounded to the one wei the branch is written for, leaving the rest to cover. Actual: the call succeeds, the keeper burns 291.697 imdUSD and receives 1 raw unit of collateral, position debt 0, totalBadDebt 0. test/scratch/BiteDust.t.sol (test_dustBranchBurnsTheWholeRepaymentForDust) reproduces it.","severity":"info","snippet":"            collateralSeized = position.collateral;","title":"bite's dust branch accepts any debtToRepay, so a liquidator who repays the full debt against sub-one-wei dust burns it all for that dust"},{"citation":"resolved","description":"`lock`, `lockIMD` and `wipe` clear a mark through `_clearIfRecovered`, which requires `_priceAgrees()` (fresh primary, NHI, collateral price and spot, and spot within skew of the primary) and health at that price. While any feed is stale or the two price feeds diverge, a marked borrower who tops up or repays above mat keeps the mark; it then stays actionable (grace already elapsed) the moment feeds are fresh again if the price is below the recovery, with no new grace. `_clearIfRecovered`'s own NatSpec states this correctly; the summary on `heel`, and the bite NatSpec's defence line ('a marked borrower who tops up or repays above mat clears the mark'), do not carry the condition. Doc-only: state the fresh-and-agreeing condition on `heel` and in the bite defence, or have the site tell a marked borrower to call `heel` once feeds are fresh.","line":1320,"path":"src/CDPVault.sol","reproduction":"Position marked underwater. Spot feed goes stale (SPOT_MAX_AGE is one hour and spot is bought on demand). Price recovers on the primary and the borrower calls lock(amount) bringing the ratio above mat. Expected per the heel NatSpec: the mark is cleared by the deposit. Actual: `_clearIfRecovered` returns without clearing because `_priceAgrees()` is false (spot stale); `liquidationMarks[owner].marked` stays true.","severity":"info","snippet":"    /// when recovery is observed; deposit, repayment and successful borrowing/withdrawal also clear them.","title":"heel NatSpec says deposit and repayment clear a mark; they clear it only on a recovery observed at a fresh, agreeing price"},{"citation":"resolved","description":"Two header comments describe properties of earlier releases: line 6 says the operator 'completes the two one-time links and operates the mock faucets' (the mainnet vault creates its imdUSD and work oracle in its constructor; there are no links to complete and no faucet), and line 13 says FEE_RECIPIENT 'MUST NOT be the feed's reporter' (there is no reporter: `SwarmFeed.report` was removed, as lines 79-83 of the same file say). The relayer half of line 13 is also moot on the shipped feeds, whose relayer is the permissionless SwarmRelay. Nothing on chain depends on these lines; they can mislead a reader of the deploy commit about who holds what. Also stale in the same family: OracleAsker.sol line 52 quotes a paid update at ~$4.25 (0.5 IMD at the parameters doc's $10.92 is about $5.46).","line":13,"path":"src/DeploymentConfig.sol","reproduction":"Read src/DeploymentConfig.sol lines 4-7 and 10-14 against the mainnet path: `ParameterizedVault` is deployed with `stablecoin_ = 0` and `WORK_ORACLE_SENTINEL` (script/DeployMainnet.s.sol `_vaultInit`), so `ImdUSD.setVault` and `MockWorkOracle` are never used; `grep report src/SwarmFeed.sol` shows no reporter function (DeployMainnet.verifyFeeds asserts `report(uint256)` is unreachable). Expected: comments describe the shipped roles. Actual: they describe the Sepolia release's.","severity":"info","snippet":"/// MUST NOT be the feed's reporter or relayer — whoever sets the price would otherwise profit from","title":"DeploymentConfig describes an operator role and a reporter role that the mainnet code no longer has"}],"hash":"49bc26e7e1e7585c0ca97f6eb73a1b4b935101b987c3cfa37b9699ab42d3ba71","nodeId":"1b99139d-b135-4f26-af82-e2b3625687d1","outcome":"completed","summary":"Both reproductions pass and the findings file holds three substantiated entries. Final report follows.\n\n## Result\n\nNo critical, high or medium defect found at c7d50ee. Every mechanism in scope was read in full and traced; the full suite passes at the pinned commit (644 passed, 4 skipped). The findings file holds three info items, two of them with scratch reproductions under `test/scratch/`. The accepted bounds were checked and hold as stated.\n\n## Answers, by question\n\n**1. Oracle.** Attestation checks are complete: relayer pin, data chain, panel floors, answer type, expiry, issue-time ordering, replay by requestId, EIP-712 domain per feed and chain, question binding by prefix plus signed window, span bounds, advancing and recent toBlock. The epoch rule anchors at the value the epoch opened on, the stale base is earned only by a whole hour of silence measured from the later of signature and relay, and a wide epoch closes behind its first value. The first value is bounded only by `verifySeeded`, as documented. Chainlink staleness, a future timestamp, a malformed round and an absurd answer all read as zero and halt rather than misprice. The asker's triggers, back-off, in-flight slot and budget are consistent; refused Treasury purchases back off two hours, caller-paid ones do not. I found nothing that lands a value the question does not support or spends the budget for nothing.\n\n**2. Borrowing and liquidation.** Every ordering I traced keeps debt backed: health is checked at mat on draw and priced free, redemption never worsens a candidate's ratio, partial bites raise the ratio above 120% and drain below it, the sweep realizes dust, and `cover` only ever burns Treasury imdUSD against recorded bad debt or a re-lock worth less than it. The held-down pool bound in `bite` is arithmetically right (1.5x at one 20% step, positions under 212%; 1.875x at two). The one sharp edge is the dust branch of `bite`, which takes any `debtToRepay` for sub-one-wei collateral; the loss is the caller's own, so info.\n\n**3. Redemption and paced figures.** The payout is min(live, paced) backing times (1 - fee) over the higher of attested and paced price, read before the burn and with this transaction's capital excluded. The reserve route and candidate route use one price. After an honest fall the two lags compound (backing at the new price over the old paid price), which only underpays. The fee base is the stored paced supply within a transaction, so splitting a burn does not escape the ratchet, and fresh self-redemptions do not move the rate.\n\n**4. Treasury.** Each exit is bounded as documented, bad debt first on imdUSD, collateral and listed assets refused to the operator, the oracle leg topped up to one day's budget with a fixed-stipend unwrap, and the new transient guard covers every paying function. `sync` is open and cannot double-credit.\n\n**5. Governance and work minting.** Every bound in Parameters is a constant; nothing applies faster than 48 hours and the only instant action is cancel. `drip` runs before a rate change. With the wage on, `backedDebt` is capped by live debt, transaction-start debt and the paced debt, and the revised netting errs low as claimed. The listing trust assumption is stated where it lives.\n\n**6. Deployment and launch window.** `run`, `verifySeeded`, `runVault` and `verify` check wiring both ways, the economics, the asker's bodies and triggers, the pool id in the bodies and the Chainlink leg. Between stages a stranger can only relay a first value or buy attestations with their own IMD; `verifySeeded` catches a value off the pool or the reference. Day-one halts are the price feed at one hour, NHI at a day, Chainlink at two hours and a spot-primary divergence; each recovers with one purchase, and the runbook's keeper and asker prefund cover them.\n\n**7. Reentrancy.** Every vault function that moves tokens is guarded and writes state before transfers; `pace` and `drip` only move paced figures and the index. Treasury paying fun","treeHash":null,"usage":{"cachedInputTokens":5672263,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":106645,"runtime":"claude","turns":43,"wallClockMs":1586615}}],"verification":[]}