{"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":"10a1f886-93ff-4ee6-bbe9-24faba0aad75","kind":"audit","nodes":[{"acceptedSubmissionHash":null,"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":"ready"},{"acceptedSubmissionHash":"df5804921905731716b60849e19826276391d02e61d3d82b9f64fa70c868ec95","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":null,"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":"waiting"},{"acceptedSubmissionHash":"c517899555f6a14b1837e454c7cdabddfe54af391dedbfef66f3b4dc68e930e3","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":null,"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":"failed"}],"objective":"Audit governance and the Treasury: src/Parameters.sol, src/Governed.sol, src/Treasury.sol and src/TreasuryFactory.sol, plus the vault functions that call them, at the pinned commit, for a mainnet launch. Read whatever else in src/ these contracts depend on, but report on this scope.\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. Timelock: can any change (economics, reserve listing, wage, gap, earnMat, oracleBudget, redemptionDivisor, and the stream's payee and daily cap) apply sooner than 48 hours, outside its bounds, or be applied by anyone other than the documented route? Can a pending proposal be held hostage or applied at a chosen later moment to harm borrowers?\n2. Treasury exits: withdraw (refuses the collateral and every listed reserve asset, and keeps imdUSD at or above totalBadDebt), withdrawNative, payStream (governed payee, capped per UTC day, never below totalBadDebt), fundOracle, redeemIMD, and cover burning Treasury imdUSD. Enumerate every way value leaves the Treasury and show each is bounded as documented. Can the stream exceed its cap through rounding, day boundaries or a rate change?\n3. Accounting: sync, lastSynced and totalReceived across ERC-20 and native ETH, including tokens that arrive between calls and the share-unwrapping path in fundOracle. Can revenue be double-counted or lost from the record?\n4. Reserve valuation: reserveValueUsd and reserveValueOf across listed assets with different decimals and feeds, including the vault's own 24-decimal collateral valued per 1e18 raw units. Can a listed feed or token make the sum revert, inflate, or misvalue?\n5. TreasuryFactory: can anyone obtain a Treasury that a vault trusts, or a vault whose Treasury another caller controls?\n6. Launch fee hand-off: can it redirect anything other than future fees, or be called by anyone but the operator?\n7. Bad debt first: can the operator or the stream ever take imdUSD that outstanding bad debt needs, for example by ordering withdraw, payStream and cover within one transaction or across a day boundary?\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":"f8502ebe0bd84872f098c3ca41225a5b99a5cbe00fb9e423dcd8b69b5547ac13","previousHash":"2a9b47c6b9072fd7fc7d6055d292481f598f1d37532cbca6cf4954d27b89e464","projectId":"10a1f886-93ff-4ee6-bbe9-24faba0aad75","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":"52120","feedbackHash":"7446314730f887e8b00363b1c595f4d2b5c8cc15d4e60fad000f0821e746dd3f","nodeKey":"audit_flow","submissionHash":"df5804921905731716b60849e19826276391d02e61d3d82b9f64fa70c868ec95","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51444","feedbackHash":"0d45157b0986f495fa28f07a73d26dcc578a10f9fdc2fb804d42a77182298c82","nodeKey":"audit_math","submissionHash":"c517899555f6a14b1837e454c7cdabddfe54af391dedbfef66f3b4dc68e930e3","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"53509e46a6fb1f2d5124fafc053468b9c050184b1b03425b9294a1c5f09d0b38","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a2eadfa3e677016f","findings":[],"hash":"220fd9f2bead951a488d30b0b624d5e781376fe1fbaffdcceebe75f4cd17f649","nodeId":"6da5b3d1-4d95-468a-a48f-da4566d39908","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6.1-sol","outputTokens":0,"runtime":"codex","turns":5,"wallClockMs":1211021}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"aa417d85641c229d","findings":[],"hash":"2e99ee81c1b8dc562ba5b2a90a78733a56e25eb532c870ed1b92dda1b35987dc","nodeId":"1ff27b28-0233-4855-a40a-85909423db8f","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":5,"wallClockMs":493441}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9c6767b941fcfedc","findings":[],"hash":"be9bc257f158eb3a89dadd035d3696c5afb5af038de8d2af2f3cf40b5a23b8d2","nodeId":"1ff27b28-0233-4855-a40a-85909423db8f","outcome":"failed","summary":"runtime reported <synthetic>, not the required premium model claude-fable-5-1","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"<synthetic>","outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":2802}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"b41d844dba698308","findings":[{"citation":"resolved","description":"CDPVault.cover bypasses Treasury._withdraw and never updates Treasury.totalReceived or lastSynced around its burn. Unsynced revenue burned by cover is never recorded; a previously synced balance remains too high after the burn and hides later receipts. This affects the actual ImdUSD token, without callbacks or unusual token behavior, and violates Treasury's cumulative-receipt accounting. It does not itself lose custody or inflate reserve valuation. Reachable with the committed economics after a liquidation realizes bad debt. Smallest fix: add a vault-only Treasury burn/accounting path that credits incoming imdUSD before the burn and reconciles lastSynced immediately after the burn and before fees are reminted, so subsequent fee receipts are recorded normally.","line":493,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {Parameters} from \"src/Parameters.sol\";\nimport {Treasury} from \"src/Treasury.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, TREASURY_FACTORY, CHAINLINK_ETH_USD} from \"src/DeploymentConfig.sol\";\n\ncontract ReviewToken is ERC20 {\n    constructor() ERC20(\"Review token\", \"REV\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\ncontract ReviewFeed is ISwarmFeed {\n    uint256 public value;\n    constructor(uint256 value_) { value = value_; }\n    function set(uint256 value_) external { value = value_; }\n    function isStale() external pure returns (bool) { return false; }\n    function latestValue() external view returns (uint256, uint64) { return (value, uint64(block.timestamp)); }\n    function maxAge() external pure returns (uint256) { return 1 days; }\n}\n\ncontract ReviewUsd {\n    function decimals() external pure returns (uint8) { return 8; }\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 1000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract CoverReceiptProofTest is Test {\n    address constant BORROWER = address(0xB0B);\n    address constant KEEPER = address(0xBEEF);\n    ReviewToken collateral;\n    ReviewFeed primary;\n    ReviewFeed spot;\n    ParameterizedVault vault;\n    Treasury treasury;\n    Parameters parameters;\n    ImdUSD stable;\n\n    function setUp() public {\n        vm.warp(10 days);\n        vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new ReviewUsd()).code);\n        collateral = new ReviewToken();\n        primary = new ReviewFeed(0.001 ether);\n        spot = new ReviewFeed(0.001 ether);\n        ReviewFeed nhi = new ReviewFeed(0.6 ether);\n        vault = new ParameterizedVault(address(collateral), address(0), address(0), address(primary), address(nhi), address(spot));\n        treasury = vault.treasury();\n        parameters = vault.parameters();\n        stable = vault.stablecoin();\n    }\n\n    function _open(address who, uint256 coll, uint256 debt) private {\n        collateral.mint(who, coll);\n        vm.startPrank(who);\n        collateral.approve(address(vault), coll);\n        vault.lock(coll);\n        vault.draw(debt);\n        vm.stopPrank();\n    }\n\n    function _realizeBadDebt() private {\n        _open(BORROWER, 240 ether, 100 ether);\n        _open(KEEPER, 1000 ether, 200 ether);\n        primary.set(0.0003 ether);\n        spot.set(0.0003 ether);\n        vault.bark(BORROWER);\n        vm.prank(KEEPER);\n        vault.bite(BORROWER, 60 ether);\n        (uint256 remaining,) = vault.positions(BORROWER);\n        assertEq(remaining, 0);\n        assertEq(vault.totalBadDebt(), 40 ether);\n        assertEq(vault.stabilityFeeOf(BORROWER), 0);\n        vm.prank(KEEPER);\n        stable.transfer(address(treasury), 100 ether);\n    }\n\n    function test_coverMustNotHideLaterReceipts() public {\n        _realizeBadDebt();\n        treasury.sync(stable);\n        assertEq(treasury.totalReceived(stable), 100 ether);\n        vault.cover(BORROWER, 40 ether);\n        assertEq(stable.balanceOf(address(treasury)), 60 ether);\n        assertEq(vault.totalBadDebt(), 0);\n        vm.prank(KEEPER);\n        stable.transfer(address(treasury), 30 ether);\n        treasury.sync(stable);\n        assertEq(treasury.totalReceived(stable), 130 ether, \"cover must reconcile the receipt baseline\");\n    }\n\n    function test_coverMustCreditUnsyncedRevenueBeforeBurning() public {\n        _realizeBadDebt();\n        vault.cover(BORROWER, 40 ether);\n        treasury.sync(stable);\n        assertEq(treasury.totalReceived(stable), 100 ether, \"burned revenue must still be recorded\");\n    }\n}","reproduction":"Use a ParameterizedVault with a drained position owing exactly 40e18 principal and no unpaid fees, and Treasury holding 100e18 imdUSD. This state is reachable at unchanged timestamps by opening 100e18 debt against collateral worth $240, lowering its price to 30%, using NHI <=0.60 (zero grace), and liquidating 60e18 at the committed 20% bonus, which consumes all collateral. A second borrower supplies liquidation tokens and sends 100e18 imdUSD to Treasury. Anyone calls treasury.sync(stablecoin): totalReceived=lastSynced=100e18. Anyone calls vault.cover(owner,40e18): balance becomes 60e18 and bad debt becomes zero, but lastSynced remains 100e18. Transfer another 30e18 imdUSD to Treasury, then call sync. Expected cumulative receipts=130e18 and credited=30e18; actual balance=90e18, credited=0, totalReceived=100e18. Alternatively, covering before the first sync records only the unburned remainder, losing the burned receipt from the record. Verified locally: test_coverMustNotHideLaterReceipts fails with 100e18 != 130e18; test_coverMustCreditUnsyncedRevenueBeforeBurning fails with 60e18 != 100e18 under forge test --match-path test/scratch/MathTreasuryReview.t.sol.","severity":"low","snippet":"        stablecoin.burn(payer, amount);","title":"cover burns Treasury imdUSD without reconciling its receipt accounting"},{"citation":"resolved","description":"Treasury.reserveValueOf isolates malformed/reverting feed and balance reads, but not arithmetic on successfully decoded values. It computes the full undiscounted value before applying the haircut. A listed feed can therefore make the entire reserve sum, ParameterizedVault.earnLine and cash revert even when its haircut is zero, or when the discounted result fits uint256. This contradicts the documented zero-factor and failed-entry isolation guarantees. Reachability: the listing and these values are permitted by the committed contracts without changing constants, but require an authorized listed dependency to return extreme values; not reachable from honest prices and balances of the documented sIMD-only launch reserve. No permissionless listing bypass is claimed. Smallest fix: return zero for a zero haircut before external valuation, compute the discounted value without a uint256 gross intermediate, and fail closed on out-of-range individual values rather than letting one bad entry revert all valuation.","line":217,"path":"src/Treasury.sol","reproduction":"Governor lists an ordinary 18-decimal ERC20 with a feed initially returning isStale=false and latestValue=(1e18,current timestamp), haircutBps=0; wait the full 48 hours and apply. Give Treasury 2e18 raw tokens. The already-listed feed now returns (type(uint256).max,current timestamp), with correct ABI encoding. Any caller calls reserveValueOf(token), reserveValueUsd or vault.earnLine. Expected: this zero-haircut entry contributes 0; actual: MathOverflowedMulDiv(), because 2e18*(2**256-1)/1e18 exceeds uint256 before multiplication by zero. Nonzero variant: haircutBps=5000 and price=2**255; the correct discounted result is 2**255, which fits, but gross=2**256 reverts. Both variants reproduced as failing assertions in test/scratch/MathTreasuryReview.t.sol.","severity":"low","snippet":"        uint256 marked = Math.mulDiv(balance, price, 10 ** entry.decimals);\n        return Math.mulDiv(marked, entry.haircutBps, BPS);","title":"Reserve valuation can overflow before applying a zero or protective haircut"},{"citation":"resolved","description":"The following source comments do not describe the committed behavior. Parameters.sol:71-74 uses mat=150, a 5000-bps backing cliff and 120% backing; the deployed mat floor is 170, the cliff is 7000 bps and the corresponding empty-reserve ratio at earnMat=2500 is 136%. Parameters.sol:116 describes totalDebt as the ratio base; ParameterizedVault uses backedDebt, excluding recorded bad debt and same-transaction additions. Parameters.sol:130-134 and :154 say vault is always the creator and is needed to validate a ceiling against outstanding debt; the constructor accepts any nonzero vault argument, and Economics validation deliberately has no outstanding-debt check. The deployed ParameterizedVault does pass itself, so this is not the previously fixed binding vulnerability. Parameters.sol:150-153 says the starting ceiling is unlimited; it is LINE=1_000_000e18. Parameters.sol:217-220 documents reserve listing immediately above proposeRedemptionDivisor; the actual listing entry is proposeReserveAsset at :230. Treasury.sol:437-438 attaches redemption documentation to the oracle-day section; :445-448 calls fundOracle the third and last exit and the only keyless exit, although payStream, cash->redeemIMD and cover are also permissionless routes. ParameterizedVault.sol:171-172 promises reserveValue is zero whenever the USD leg is unusable; independently priced listed assets remain valued. ParameterizedVault.sol:218, CDPVault.sol:179, :779 and :882 still describe a 10% liquidation bonus or a 110% payout, while CHOP_PERCENT=20 produces 120%. CDPVault.sol:28-30 and :279 assume 18-decimal MockIMD and USD denomination in the base vault; the deployed collateral is 24-decimal sIMD, and the base vault consumes ETH-denominated primary prices. In particular the IMD terminology in Treasury.sol:437, ParameterizedVault.sol:36, :120, :129 and CDPVault.sol:508 describes collateral-unit payouts that are sIMD on mainnet, not unwrapped IMD. CDPVault.sol:889-891 says the current-price shortfall helper is shared with the recorded accumulator and they cannot disagree; _recordBadDebt instead records debtOf only for drained positions. ImdUSD.sol:61 says the vault burns only a caller's tokens; cover burns the Treasury's tokens. DeploymentConfig.sol:99-104 says the economics have no setter and stability-fee changes require redeployment; Parameters changes them after its timelock. DeploymentConfig.sol:144-148 repeats the old totalDebt formula, 150% floor and 120% ratio. In the pricing dependency, SharePriceFeed.sol:8-13 promises any ERC-4626 rate only increases and a position repairs itself despite stability fees; neither monotonic exchange rates nor yield exceeding the debt rate is enforced. SharePriceFeed.sol:28-34 and :57 say both reads are raw and any unreadable leg returns zero; assetFeed.latestValue/isStale are typed calls and propagate failures. These are documentation defects, not additional privilege bypasses. Smallest fix: update the claims and attach function NatSpec to the correct functions; preserve the intended economic constants and permissions. The no-expiry issue already reported in the earlier audit is excluded from this finding.","line":150,"path":"src/Parameters.sol","reproduction":"Concrete checks at the committed constants: (1) deploy the normal vault and read parameters.line(): actual 1_000_000e18, not uint256.max. (2) At NHI=0.85e18, mat()=170; 170/1.25=136%, not 120%. (3) At a $1 collateral price, bite repayment 10e18 seizes 12e18 collateral, not the documented 11e18. (4) A caller can deploy new Parameters(ICheckpointedVault(address(0xBEEF))); vault() is 0xBEEF regardless of the deploying address. (5) List an independently priced reserve worth 12e18 USD, then make Chainlink unusable: reserveValue() remains 12e18; the existing test_zeroReservePriceAndOneStaleAssetDoNotValueOtherAssetsIncorrectly passes with that behavior. (6) The reproduced cover(owner,40e18) burns 40e18 from Treasury when the caller owns no imdUSD. (7) A position with 100e18 debt and collateral worth $60 at CHOP_PERCENT=20 has current badDebtOf=50e18, while totalBadDebt stays zero until collateral is drained. (8) With an unchanged share rate over one year, 100e18 debt accrues to 104.44e18 at the committed duty=444, so its collateral ratio falls even though the share rate never decreased. (9) Construct SharePriceFeed with a share vault returning a positive rate and a code-bearing asset feed whose latestValue reverts; constructor succeeds, latestValue reverts rather than returning zero. The mainnet-created UsdPriceFeed is not claimed to exhibit this arbitrary-feed failure.","severity":"info","snippet":"    /// @dev Seeded from the shipped constants, so a fresh Parameters is exactly the configuration\n    /// the vault would have had with them compiled in — including the unlimited default ceiling,","title":"Governance and Treasury NatSpec still describes superseded arithmetic and guarantees"}],"hash":"c517899555f6a14b1837e454c7cdabddfe54af391dedbfef66f3b4dc68e930e3","nodeId":"9df83535-cecc-4405-b80f-7c39fd47db33","outcome":"completed","summary":"Saved [`.imd-findings.json`](/home/ns/.identitymd/work/10a1f886-93ff-4ee6-bbe9-24faba0aad75/9df83535-cecc-4405-b80f-7c39fd47db33/.imd-findings.json): **two low-severity defects and one informational documentation finding**. Contracts and configuration remain unchanged.\n\nThe defects are:\n\n- **Low — `CDPVault.cover` loses Treasury receipt accounting.** Sync 100 imdUSD, cover 40, then receive 30: `totalReceived` remains **100 instead of 130**. Burning before the first sync also loses the burned receipt. This is reachable with committed economics and ordinary ImdUSD. Reconcile Treasury accounting immediately before and after the burn, before reminting fees. A verified failing Foundry proof is embedded.\n- **Low — `Treasury.reserveValueOf` can overflow before applying its haircut.** With balance `2e18`, decimals 18, price `uint256.max` and haircut zero, valuation reverts instead of contributing zero. A 50% haircut also fails for price `2**255`, although the discounted result fits. This requires an extreme-valued listed dependency; normal sIMD launch balances and prices do not trigger it. Skip zero factors and handle arithmetic bounds before an entry can revert the entire valuation.\n\nAnswers to the seven questions:\n\n1. **Timelock.** No early application or unauthorized modification was found. All eight proposal types use the same governor-only proposal route and permissionless application after 48 hours, with validation repeated at application. Bounds cover duty ≤1,000 bps; skew 100–2,000; chip + cut ≤10,000; positive line; earnMat ≤2,500; wage ≤1 imdUSD/task; gap 25–100; oracle budget ≤100 IMD/day; divisor 1–8; and stream ≤500 imdUSD/day with a nonzero payee when enabled. The previous outstanding-debt hostage condition is removed. A reserve dependency becoming invalid can prevent application until cancellation. **Matured proposals still have no expiry**, so later execution remains possible; this is the previously documented issue, excluded from new findings.\n\n2. **Treasury exits.** The complete local exit surface is:\n   - `withdraw`: operator only; rejects collateral and listed reserves; preserves recorded bad-debt funds.\n   - `withdrawNative`: operator only, bounded by available ETH.\n   - `payStream`: permissionless trigger, governed recipient, daily allowance and available surplus.\n   - `fundOracle`: permissionless trigger, fixed asker, governed daily IMD budget; unwraps shares through `withdraw`.\n   - `redeemIMD`: vault only, reached through `cash` after pricing, payout checks and burning the caller’s imdUSD.\n   - `cover`: permissionless, restricted to recorded bad debt on drained positions; burns Treasury imdUSD, with fees retired and reminted first.\n   - `handOffLaunchFees`: operator-controlled redirection of the external requester role.\n\n   Stream payments use exact integer amounts. Changing the rate or payee does not reset `streamPaid`. Midnight permits a new UTC-day allowance, including two allowances close together across midnight, as documented. Lowering a cap cannot reverse payments already made.\n\n3. **Accounting.** Ordinary ERC-20 withdrawals and native withdrawals credit unsynced arrivals before moving their baselines. Repeated syncs do not duplicate ordinary receipts. The share-unwrapping path records incoming shares and resets the baseline to remaining shares. **`cover` bypasses this bookkeeping**, producing the confirmed finding above. Generic rebasing or callback-bearing tokens require additional assumptions; the accounting is based on observed balances.\n\n4. **Reserve valuation.** Ordinary assets use their stored decimals; the vault’s collateral deliberately uses denominator `1e18`. Thus one `1e24`-unit sIMD share worth 7.95 IMD is valued correctly through the raw-unit share price. Haircuts round conservatively. Ordinary reverts and malformed responses are isolated, but extreme decoded values can trigger the reported arithmetic failure. A lying listed feed or token can also inflate reported backing: governance’s cho","treeHash":null,"usage":{"cachedInputTokens":4251392,"inputTokens":233191,"model":"gpt-6-astra","outputTokens":26745,"runtime":"codex","turns":7,"wallClockMs":1379680}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a5c5e95a2ed07117","findings":[{"citation":"resolved","description":"Q7 (bad debt first). `cover` refuses any position whose collateral is nonzero, but `lock` has no health check and nothing stops the defaulted owner from depositing one raw unit back into the drained position. After that: (1) `cover` reverts `NoRealizedBadDebt` for every amount; (2) `_recordedBadDebt[owner]` and `totalBadDebt` keep the full realized figure (the `collateral != 0` branch in `_reduceDebt` only ever lowers it on repayment, which the defaulter will not do); (3) `bite` cannot drain the dust either at mainnet pricing, because one wei of debt seizes floor(1.2e18 / price) raw units — with sIMD priced per 1e18 raw units at about 8.7e13 that is ~13,800 units, more than the 1 held — so `InsufficientCollateral` for every `debtToRepay >= 1`, and the sweep in `bite` never runs; (4) `free(1)` is refused because the position is unhealthy. Consequences, all by design of the new guards but now unremovable by anyone: `Treasury.withdraw` of imdUSD reverts `BadDebtFirst(totalBadDebt)` for everything above the stuck figure, `payStream` pays nothing out of it, `backedDebt()` and `_securedCollateralValue` stay reduced by it (lower work ceiling, lower `backingPerUnit` and so lower `cash` payouts than the covered state would give), and the Treasury's imdUSD stays in `totalSupply`. Cost to the defaulter: one raw unit of sIMD (about 1e-20 sIMD) plus gas; if a liquidator ever does manage a dust bite, re-locking in the next block repeats it. Reachable with the committed constants (CHOP 20%, sIMD per-1e18-raw pricing). Expected: realized bad debt can always be retired from surplus. Actual: the defaulter decides whether it ever can be. Smallest fix: gate `cover` on `_recordedBadDebt[owner] != 0` alone and cap `amount` at `_recordedBadDebt[owner]` (the realized figure `_reduceDebt` already maintains with `min(previous, current)` when collateral is present), or alternatively let `bite` seize the whole remainder when `position.collateral < mulDiv(1, (100 + CHOP_PERCENT) * 1e16, price)` so the sweep that makes bad debt reachable also covers dust re-locks.","line":488,"path":"src/CDPVault.sol","reproduction":"WorkBackingFixture (ParameterizedVault, price $1 per 1e18 raw, mat 170). BORROWER lock 170e18, draw 100e18; KEEPER lock 450e18, draw 250e18. Set price 0.5; bark(BORROWER); warp 6h; KEEPER bite(BORROWER, 170e18*0.5/1.2 = 70.83e18) -> position collateral 0, totalBadDebt = bad ~29.2e18 + fees. Price back to 1. Fund Treasury with bad + 5e18 imdUSD. BORROWER: collateral.approve(vault, 1); vault.lock(1). Then: vault.cover(BORROWER, 1e18) reverts NoRealizedBadDebt (expected: covers). totalBadDebt() == bad unchanged. Operator withdraw(imdUSD, op, 5e18) succeeds; withdraw(imdUSD, op, 1) reverts BadDebtFirst(bad) forever. Set price to 1e14 (sIMD-like per 1e18 raw), bark, warp 6h: KEEPER bite(BORROWER, 1) reverts InsufficientCollateral (seized = 1.2e18/1e14 = 12,000 > 1). Scratch test test/scratch/Leads.t.sol::test_dustRelockBlocksCoverAndLocksOperatorImdUSD passes on this code (i.e. demonstrates the lock-out).","severity":"medium","snippet":"        if (_positions[owner].collateral != 0 || _recordedBadDebt[owner] == 0) revert NoRealizedBadDebt();","title":"cover: a drained borrower re-locks one wei and its realized bad debt becomes permanently uncoverable, locking the Treasury's imdUSD above it and stalling the stream"},{"citation":"resolved","description":"Q3 (accounting). Every other exit from the Treasury (`_withdraw`, `_withdrawUnderlying`, `withdrawNative`) first credits what arrived since the last sync and then moves the baseline, precisely because audit c71449d1 found that clamping `lastSynced` downward drops revenue from the record. `cover` is a new exit that bypasses the Treasury entirely: the vault burns `amount` straight out of the Treasury's imdUSD balance, then mints `feePaid` back to it. Stability fees arrive at the Treasury by mint (from `wipe`/`bite`/`cover`), not by a call that syncs, so between syncs the balance is normally above `lastSynced`. When `cover` lowers the balance, the next `sync(imdUSD)` sees `balance <= counted` (or a smaller excess), clamps the baseline and credits nothing (or less), and the receipts that had arrived are never counted. The `feePaid` minted back by `cover` is likewise never recorded as received. No funds move wrongly; the running total this contract exists to answer is permanently understated by up to the covered amount per cover, and `cover` is permissionless so this recurs whenever bad debt exists. Same class as the previously fixed c71449d1 low, regressed through a new path. Smallest fix: before `stablecoin.burn(payer, amount)` in `cover`, call a hook that ParameterizedVault overrides as `treasury.sync(stablecoin)` (sync is permissionless and idempotent), and sync again after the fee re-mint or accept that the re-mint is credited by the next sync (it will be, since it raises the balance above the clamped baseline).","line":493,"path":"src/CDPVault.sol","reproduction":"Same drain as above (realized bad debt `bad`). KEEPER transfers 10e18 imdUSD to the Treasury; sync -> totalReceived = R, lastSynced == balance. KEEPER transfers another 30e18 (unsynced). Anyone calls vault.cover(BORROWER, 20e18). sync(imdUSD) now credits 10e18 (balance rose 30, fell 20). Expected: totalReceived - R == 30e18 (30 arrived). Actual: 10e18. Scratch test test/scratch/Leads.t.sol::test_coverLosesUnsyncedReceiptAndFeeRemint fails on this code with `10000000000000000000 != 30000000000000000000`.","severity":"low","snippet":"        stablecoin.burn(payer, amount);","title":"cover burns Treasury imdUSD outside the Treasury's receipt accounting, so unsynced stability-fee receipts (and the fee re-mint) vanish from totalReceived"},{"citation":"resolved","description":"Q4 (reserve valuation). The three reads are isolated with raw staticcalls so a dead or malformed feed counts for nothing, and the NatSpec at lines 197-199 says a listed feed 'can never inflate it, and it never makes this view revert'. The arithmetic after the reads is not isolated: `Math.mulDiv` reverts when `balance * price / 10**decimals` does not fit in 256 bits, and `_readValue` accepts any uint256 as the price as long as the second word fits a uint64. A listed source that returns a price above about 2^256 / balance * 10**decimals (for 1e24 raw units of an 18-decimal asset, any price above ~1.2e71) therefore reverts `reserveValueUsd()`. That revert propagates to `ParameterizedVault.reserveValue()`, `earnLine()` (every `earn` reverts), `_redemptionReserveBacking` and `_backingPerUnit` (every `cash` and the public `backingPerUnit()` revert once supply is nonzero), and stays until a delisting matures 48 hours later. Reachability: needs a listed feed or token to misbehave (the launch register holds only sIMD priced by the vault's own SharePriceFeed, whose value is bounded by the share vault's exchange rate times the attested price), so this is a third-party-dependency failure rather than an unprivileged attack; the defect is that the code does not have the property it documents, and the direction of failure (halting redemption) is the one the design elsewhere goes to lengths to avoid. Smallest fix: check `price <= type(uint256).max / balance` (or compute with a saturating helper) and return 0 when the product would overflow, matching the documented 'counts for nothing' behaviour; the sum in `reserveValueUsd` can saturate the same way.","line":217,"path":"src/Treasury.sol","reproduction":"WorkBackingFixture. KEEPER opens a position (supply > 0). Governor lists an 18-decimal ReserveTestToken with a TestSwarmFeed at 1e18, haircut 10000, apply after 48h. Mint 1e24 raw of the token to the Treasury. Feed.setValue(type(uint256).max / 1e5) (a well-formed (uint256,uint64) answer; isStale false). Expected per NatSpec: reserveValueUsd() returns some value (at worst 0 for this asset). Actual: reserveValueUsd() reverts MathOverflowedMulDiv; earnLine() reverts; backingPerUnit() reverts; cash(1e18,0,0) reverts; earn(1e18) reverts. Scratch test test/scratch/Leads.t.sol::test_reserveValueRevertsOnHugePrice passes on this code (each expectRevert is met).","severity":"low","snippet":"        uint256 marked = Math.mulDiv(balance, price, 10 ** entry.decimals);","title":"reserveValueOf reverts (MathOverflowedMulDiv) on a listed feed answering a very large well-formed price, taking reserveValueUsd, earnLine, backingPerUnit, cash and earn down with it despite the NatSpe"},{"citation":"resolved","description":"Q4, governance-error hazard rather than an unprivileged path. The register special-cases the vault's collateral so its 24-decimal balance is priced per 1e18 raw units, but it accepts any ISwarmFeed for it. The vault exposes two feeds that both validate: `usdPriceFeed` (USD per 1e18 raw IMD, ~10.92e18) and `collateralPriceFeed` (USD per 1e18 raw sIMD = convertToAssets(1e18) * IMD price / 1e18 = 7.95e12 * 10.92e18 / 1e18 ~ 8.68e13). The runbook's listing step names the right one; a proposal naming the wrong one is visible for 48 hours but nothing on chain refuses it. If applied, 1 sIMD (1e24 raw, worth ~$86.8) counts as 1e24 * 10.92e18 / 1e18 = $10.9M: the reserve term of `earnLine` is inflated ~126,000x and work minting is bounded only by rights, and `_redemptionReserveBacking` counts `others` for non-gem assets only, so redemption is unaffected. Smallest fix: in `validateReserveAsset`, when `asset == gem`, require `priceFeed == vault.collateralPriceFeed()` (the vault already exposes it), the same way decimals are pinned.","line":188,"path":"src/Treasury.sol","reproduction":"Governor: parameters.proposeReserveAsset(sIMD, vault.usdPriceFeed(), 10000); 48h; applyPending() succeeds (validateReserveAsset probes isStale/latestValue only). Treasury holds 1e24 raw sIMD. reserveValueOf(sIMD) = mulDiv(1e24, 10.92e18, 1e18) = 1.092e25 (USD 1e18-scaled, i.e. $10.9M). Expected with collateralPriceFeed: mulDiv(1e24, 8.68e13, 1e18) = 8.68e19 ($86.8). earnLine() jumps by the difference.","severity":"info","snippet":"        uint8 places = address(asset) == _linked(abi.encodeWithSignature(\"gem()\"))","title":"setReserveAsset pins the collateral's decimals but not its price source: listing sIMD with usdPriceFeed instead of collateralPriceFeed passes validation and over-values the reserve ~126,000x"},{"citation":"resolved","description":"Stale claim. The Treasury now has seven value exits: withdraw, withdrawNative, payStream, fundOracle, redeemIMD, the vault's cover (burn), and handOffLaunchFees (future fees). `payStream` is also keyless (anyone may call it), so fundOracle is neither the last way out nor the only keyless one. The property the reader is asked to rely on (one unkeyed outflow, capped) is not what the code has; payStream is capped by the same mechanism but is a second unkeyed outflow in imdUSD.","line":446,"path":"src/Treasury.sol","reproduction":"Read the cited line against the code it documents; see description for the exact divergence (the compiled behaviour is the code's, not the comment's).","severity":"info","snippet":"    /// @dev The Treasury's third and last way out, and the only one with no key behind it: the","title":"NatSpec claims a property the code does not have: src/Treasury.sol:446"},{"citation":"resolved","description":"Orphaned NatSpec. These two doc lines describe `redeemIMD` but are followed by the oracle-budget section comment and the `oracleDay` state variable, so the compiler attaches them to `oracleDay` and `redeemIMD` (line 511) carries no documentation. Also 'IMD' is sIMD on mainnet: the function moves `gem`, whatever token that is.","line":437,"path":"src/Treasury.sol","reproduction":"Read the cited line against the code it documents; see description for the exact divergence (the compiled behaviour is the code's, not the comment's).","severity":"info","snippet":"    /// @notice Release reserve IMD for a redemption priced and burned by this Treasury's vault.","title":"NatSpec claims a property the code does not have: src/Treasury.sol:437"},{"citation":"resolved","description":"Misplaced NatSpec. This block describes `proposeReserveAsset` (the Treasury's rules apply at proposal, imdUSD refused, haircut at most 10000) but is attached to `proposeRedemptionDivisor` at line 221; `proposeReserveAsset` at line 230 has no doc. ABI consumers reading docs/abi get the reserve-asset description on the divisor function.","line":217,"path":"src/Parameters.sol","reproduction":"Read the cited line against the code it documents; see description for the exact divergence (the compiled behaviour is the code's, not the comment's).","severity":"info","snippet":"    /// @notice Queue a listing, repricing or (with a zero price source) delisting of one of the","title":"NatSpec claims a property the code does not have: src/Parameters.sol:217"},{"citation":"resolved","description":"Stale numbers after the 2026-10-05 parameter change: `_mat` floors at 170 (CDPVault.sol:1082), not 150, so the cliff `mat - 1` the comment refers to is 7000 bps, not 5000, and MAX_EARN_MAT_BPS = 2500 is now about 36% of that cliff rather than half of it. The same stale '5000 at the loosest NHI' appears in src/DeploymentConfig.sol:146. The direction is conservative (the real bound is looser than the documented one, so the constant is safer than claimed); only the claim needs updating, not the constant.","line":72,"path":"src/Parameters.sol","reproduction":"Read the cited line against the code it documents; see description for the exact divergence (the compiled behaviour is the code's, not the comment's).","severity":"info","snippet":"    /// touches one at the loosest NHI (mat 150); this is half of it, 120% with an empty reserve.","title":"NatSpec claims a property the code does not have: src/Parameters.sol:72"},{"citation":"resolved","description":"Stale rationale. The ceiling-against-outstanding-debt check was deliberately removed (Parameters.sol:408-417), so the example given for why `_validate` runs twice no longer exists. The second validation still matters, but for a different reason: the ReserveAsset payload re-probes the asset's `decimals()` and the feed's `isStale()/latestValue()` at application, so a token or feed that changes shape during the 48 hours is refused then. Worth stating, because that is also the one place a third party (the listed token's or feed's controller) can block an application and force a cancel.","line":20,"path":"src/Governed.sol","reproduction":"Read the cited line against the code it documents; see description for the exact divergence (the compiled behaviour is the code's, not the comment's).","severity":"info","snippet":"/// because a bound that reads live state (a ceiling against outstanding debt) can hold when proposed","title":"NatSpec claims a property the code does not have: src/Governed.sol:20"},{"citation":"resolved","description":"Stale after CHOP_PERCENT moved to 20: `bite` seizes floor(debtToRepay * 1.2e18 / price), collateral worth 120%. The same '10% bonus' wording is at line 177 (`cut` doc) and line 779 is the one a liquidator reads to size `debtToRepay`. The code is right; the comment mis-sizes the seizure by a tenth.","line":779,"path":"src/CDPVault.sol","reproduction":"Read the cited line against the code it documents; see description for the exact divergence (the compiled behaviour is the code's, not the comment's).","severity":"info","snippet":"    /// @dev Payout is floor(debtToRepay * 1.1e18 / price) IMD, i.e. collateral worth 110% of the imdUSD burned","title":"NatSpec claims a property the code does not have: src/CDPVault.sol:779"}],"hash":"df5804921905731716b60849e19826276391d02e61d3d82b9f64fa70c868ec95","nodeId":"de125ecd-794c-40b6-8cf8-e21cfc268e69","outcome":"completed","summary":"Findings are written to `.imd-findings.json` (10 entries: 1 medium, 2 low, 7 info) and every path, line and snippet was checked against the tree. Scratch tests live in `test/scratch/Leads.t.sol`; no source or config file was changed.\n\n## Answers to the seven questions\n\n**1. Timelock.** No bypass found. Every governed value (economics set, earnMat, wage, gap, oracleBudget, redemptionDivisor, reserve listing, stream payee and cap) is written only inside `_apply`, reachable only through `applyPending` after `pendingEta`, which `_propose` sets to now plus 48 hours. Bounds are checked at proposal and again at application, and the register is written only by `Parameters` as registrar. The no-expiry \"apply at a chosen moment\" hazard is the one the earlier audit already recorded and the code documents; it has not regressed. The only third-party stall is a reserve listing whose token or feed changes shape during the delay, which forces a cancel.\n\n**2. Treasury exits.** Seven ways value leaves, each bounded as documented: `withdraw` (operator, refuses collateral and listed assets, imdUSD held at or above totalBadDebt), `withdrawNative` (operator), `payStream` (anyone, governed payee, per-UTC-day cap, spare above bad debt only), `fundOracle` (anyone, pinned asker, daily IMD budget), `redeemIMD` (vault only, amount set by `cash`), the vault's `cover` burn, and `handOffLaunchFees` (future fees). The stream cannot exceed its cap through rounding, a day boundary or a mid-day rate change. I confirmed that with a scratch test.\n\n**3. Accounting.** Receipts cannot be double-counted. They can be lost: `cover` burns Treasury imdUSD without crediting arrivals first, so the next `sync` clamps the baseline and drops whatever arrived since the last sync. This is the class fixed in the earlier audit, regressed through the new path. (Low)\n\n**4. Reserve valuation.** Decimals and the per-1e18-raw collateral case are handled correctly. A listed feed answering an absurd but well-formed price makes `reserveValueOf` revert on overflow, which takes down `earnLine`, `backingPerUnit`, `cash` and `earn`, contrary to the NatSpec promise. (Low) Listing sIMD with the vault's `usdPriceFeed` instead of `collateralPriceFeed` passes validation and inflates the reserve about 126,000 times. (Info, governance error, cheap pin available)\n\n**5. TreasuryFactory.** No issue. A Treasury serves whoever called `create`, and a vault trusts only the Treasury its own constructor created.\n\n**6. Launch fee hand-off.** Operator only, fixed selector, no authority of the Treasury is delegated. Whether `setRequester` moves only future fees is a property of the external launch factory, which is not in this repository.\n\n**7. Bad debt first.** The ordering within a transaction and across days is sound. The one defect is the medium finding: a drained borrower can `lock` one raw unit of sIMD back into the position. `cover` then refuses it forever, `bite` cannot seize dust at mainnet pricing, `totalBadDebt` stays on the books, and the operator's imdUSD above it plus the stream are locked indefinitely at near-zero cost to the defaulter.\n\n## Coverage\n\nRead in full: Treasury, Parameters, Governed, TreasuryFactory, ParameterizedVault, CDPVault, DeploymentConfig, ImdUSD, UsdPriceFeed, SharePriceFeed, and all six interfaces. Read only by grep: SwarmFeed (deviation bound). Not read: OracleAsker, SwarmRelay, SwarmWorkOracle, Registry, WorkOracleFactory, the three feed leaves, and the mocks. Not reachable: the sIMD (StakedIMD) and launch-factory (PoolFees) sources, which live outside this repository.","treeHash":null,"usage":{"cachedInputTokens":3159355,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":67366,"runtime":"claude","turns":34,"wallClockMs":1089953}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"99b216f8773f1f55","findings":[],"hash":"e67b9f06fc0d60d5b2ae567e25c3051f8a18f67b7a2b8607b0d9372e83536e93","nodeId":"9df83535-cecc-4405-b80f-7c39fd47db33","outcome":"failed","summary":"{\"type\":\"error\",\"status\":400,\"error\":{\"type\":\"invalid_request_error\",\"message\":\"The 'gpt-6-astra' model is not supported when using Codex with a ChatGPT account.\"}}","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":5709}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"fd5402086dce252e","findings":[],"hash":"fd7a6990e269488fc258b66ae7bcbf0650407bd3f427540d5f93b4d744d92d13","nodeId":"1ff27b28-0233-4855-a40a-85909423db8f","outcome":"failed","summary":"runtime reported <synthetic>, not the required premium model claude-fable-5-1","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"<synthetic>","outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":2991}}],"verification":[]}