{"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":"da7d5b1c-b574-4738-b241-8c03aff321cd","kind":"skill:audit-imported-code","nodes":[{"acceptedSubmissionHash":"698403726ad6d0a1721d251f9c65791c3f2a90d932690f955d6eecbb0e20ca35","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","state":"accepted"}],"objective":"Audit the surface this repository gained after its last independent review, and only that surface. In scope: src/UsdPriceFeed.sol; the USD denomination of src/ParameterizedVault.sol (_price, _pricingStale, reserveValue, workCeiling, backedDebt and the transient slot behind it); the reserve register in src/Treasury.sol (validateReserveAsset, setReserveAsset, reserveValueUsd, reserveValueOf, registrar and the _linked probe); the work-ratio and reserve-asset proposal paths in src/Parameters.sol; the question binding in src/SwarmFeed.sol (questionPolicy, expectedQuestionHash, _requireQuestion) together with the pinned prefixes and span bounds in src/PriceFeed.sol, src/NhiFeed.sol and src/SpotFeed.sol; and in src/CDPVault.sol only the pricing seam (_price, _pricingStale, _requirePriceAgreement, workCeiling and its enforcement in mintFromWork). Everything else is out of scope: the accounting, liquidation, marking and bad-debt logic of CDPVault, the token and mock contracts, and the parts of Governed, Parameters, Registry, Treasury and SwarmRelay that an earlier audit (job c71449d1) already covered. Report findings only; change no code. Answer each of these individually, with the reasoning that settles it: (1) The vault now prices collateral in USD while the swarm feed quotes IMD in wei of ETH. Is there any remaining path where a USD-denominated figure is added to, compared against or divided by an ETH-denominated one, or where a value is 1e18-scaled twice or not at all? Name every mixed-unit expression you find. (2) _requirePriceAgreement deliberately reads the RAW primary feed instead of _price(), so the divergence guard and the pricing now read different feeds. Can a caller profit from that split, for example with a fresh primary and a dead ETH/USD leg or the reverse, and does the guard still bound what it was meant to bound? (3) _pricingStale adds the USD leg, so a dead Chainlink ETH/USD halts minting, marking and liquidation. Can that halt be induced cheaply or made permanent, and does halting liquidation strand positions that were already underwater when the leg died? (4) UsdPriceFeed reads its USD leg by low-level staticcall and reports anything malformed as zero rather than reverting. Can an aggregator return data that passes the length, sign and timestamp checks yet yields a wrong value - wrong decimals, a future timestamp, a different round, a longer tuple - and can latestValue overflow or truncate? (5) Treasury.setReserveAsset is gated on registrar(), which is resolved by staticcalling the creating vault's parameters(). Can that resolve to an address the deployer did not intend, or change after deployment? Can an asset be listed whose price source values it in a unit that is not USD, and would anything catch that? (6) Can reserveValueUsd be made to revert, to count a balance twice, to overcount through a rebasing or fee-on-transfer token, or to grow unbounded in gas so that workCeiling becomes unreadable and mintFromWork unusable? (7) backedDebt caps totalDebt at the level this transaction began with, recorded in transient storage by the first _debtChanged of the transaction, and the test suite depends on foundry isolate mode to model that. On a real chain, is there any ordering - a reentrant callback, a multicall, a batched account-abstraction operation, two entry points in one transaction - in which the snapshot is wrong in the loosening direction? (8) Each feed splices two signed decimal numbers into a pinned question prefix and recomputes the attester's hash, then bounds the window span and requires toBlock to advance. Can _requireQuestion be satisfied by an attestation answering a different question - a prefix that would canonicalise differently, a span exactly at a bound, leading zeros, or a number the signing service would render in another form - and can the advance rule wedge a feed permanently? (9) Do the fixes made for job c71449d1 close their findings without opening anything new: bindVault removed so a vault creates its own Parameters, Treasury.withdraw crediting the unsynced delta before moving the baseline, and the debt-ceiling-versus-totalDebt validation removed as griefable?","parentJobId":null,"planHash":"e0dd507ec1ce417d1c8ca9a4731f3e175b1bdec7deb5f5049c4ee0b3af16dbfd","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"da7d5b1c-b574-4738-b241-8c03aff321cd","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":"50962","feedbackHash":"a85eef382b969f4a0aa35284e5d90a9a1c4fcc26558ebaf9a6887c64968d6e9c","nodeKey":"audit_imported_code","submissionHash":"698403726ad6d0a1721d251f9c65791c3f2a90d932690f955d6eecbb0e20ca35","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"6a724fd0d789866d6a17ed2e822c0a99322aa9eeefb8703c765a901f120f671d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3f6a9bdd601cb99f","findings":[{"citation":"resolved","description":"ParameterizedVault._price() now returns USD per IMD, but _clearIfRecovered bypasses that override and reads the raw ETH-per-IMD primary feed. The collateral-ratio expression at line 648 therefore divides ETH-valued collateral by USD-denominated debt. At ETH/USD = 2000 it understates the recovery ratio by approximately 2000 times. A deposit or repayment can restore the required USD collateralization without clearing the liquidation mark; a later price decline can reuse the old grace period. This is a consequence of the new USD pricing seam. The explicit clearRecoveredMark entry point uses the correct pricing path and can clear the recovered position.","line":642,"path":"src/CDPVault.sol","reproduction":"Use 18-decimal collateral, fresh primary and spot feeds at 0.001e18, ETH/USD answer 2000e8 with decimals 8, and NHI 0.9e18 (150% minimum collateral ratio and six-hour grace). Deposit 2000e18 IMD and borrow 2000e18 COMP. Set primary and spot to 0.0005e18 and call markUnderwater(borrower). Deposit another 2000e18 IMD. Expected: collateralRatio(borrower) is 200 and the mark is cleared. Actual: the ratio is 200 but the mark remains because the automatic helper computes floor(4000 * 0.0005 / 2000 * 100) = 0. Advance six hours, set both raw feeds to 0.0003e18, and liquidate 1e18 COMP: liquidation succeeds using the previous mark instead of requiring a new mark and grace period. The earlier review reports test_regression_usdRecoveryMustClearMark failing and test_observed_retainedMarkSkipsNewGrace passing; their source is preserved in artifacts/audit.md.","severity":"medium","snippet":"            (uint256 price,) = priceFeed.latestValue();\n            (uint256 spot,) = spotFeed.latestValue();\n            uint256 difference = price > spot ? price - spot : spot - price;\n            // Invalid recovery observations preserve the mark without blocking deposits or repayments.\n            if (\n                price != 0 && spot != 0 && difference <= Math.mulDiv(price, maxDivergenceBps(), 10_000)\n                    && _collateralRatio(position.collateral, debt, price) >= minCR()","title":"USD denomination leaves automatic recovery using ETH against USD debt"},{"citation":"resolved","description":"Successful external-call results are ABI-decoded in the caller, and decoding failures are not caught by these try/catch clauses. Listing validation at lines 129-132 checks call success and response length without validating boolean or uint64 encodings. One malformed listed feed can consequently revert reserveValueUsd(), workCeiling(), and otherwise eligible mintFromWork() calls instead of contributing zero. Explicit feed reverts are caught correctly. This requires an invalid authorized listing or a listed dependency that later malfunctions; it does not grant permissionless listing authority. Governance can delist the broken feed without querying it, normally after the 48-hour proposal delay.","line":176,"path":"src/Treasury.sol","reproduction":"A: Use a code-bearing feed whose isStale() successfully returns abi.encode(uint256(2)) and whose latestValue() returns abi.encode(uint256(1e18), uint64(block.timestamp)). Through the authorized Parameters, propose an 18-decimal reserve asset with this feed and haircutBps = 10000, wait 48 hours, and apply. Both listing validation passes accept the invalid boolean. Calling reserveValueUsd() then reverts during boolean decoding, even when the token balance is zero; expected rejection at listing or zero contribution at valuation. B: List a normal fresh feed, then make latestValue() successfully return only abi.encode(uint256(1e18)). Valuation reverts because the one-word result cannot decode as (uint256,uint64). A two-word result whose timestamp is 2**64 likewise has an invalid declared encoding. Expected: the unusable feed contributes zero without reverting the entire reserve sum. The earlier review reports test_observed_malformedBoolPassesListing passing and test_regression_malformedBoolShouldFailClosed and test_regression_shortDataAfterListingShouldFailClosed failing; their source is in artifacts/audit.md.","severity":"medium","snippet":"        try entry.priceFeed.isStale() returns (bool stale) {\n            if (stale) return 0;\n        } catch {\n            return 0;\n        }\n        uint256 price;\n        try entry.priceFeed.latestValue() returns (uint256 value, uint64) {\n            price = value;\n        } catch {\n            return 0;","title":"Malformed reserve-feed return data bypasses failure isolation"},{"citation":"resolved","description":"The balanceOf call is not isolated from failure. Registration checks token code and decimals but does not check balanceOf support. A listed token that stops answering balance queries reverts the entire reserve sum, including valuations of unrelated healthy assets, and prevents work minting. The balance call occurs even when the actual balance or haircut is zero. This is conditional on listed-token behavior; the earlier review did not establish such behavior for the shipped collateral token. Delisting remains available after the governance delay.","line":188,"path":"src/Treasury.sol","reproduction":"Through Parameters, list an 18-decimal token with a fresh 1e18 USD price feed and haircutBps = 10000. After application, make balanceOf(address(treasury)) revert with \"paused balance\", while decimals() and both price-feed reads remain valid. Call vault.workCeiling(): it reverts with the balance-query failure, even if the token holds no treasury balance. A worker with positive rights and fresh vault feeds consequently cannot mint against other healthy reserves. Expected: the unusable entry contributes zero and healthy reserve backing remains readable. The earlier review reports test_regression_revertingBalanceMustNotBrickOtherReserves failing on the balance-query revert; its source is in artifacts/audit.md.","severity":"medium","snippet":"        uint256 marked = Math.mulDiv(asset.balanceOf(address(this)), price, 10 ** entry.decimals);","title":"A listed token balance-read failure blocks the entire work ceiling"},{"citation":"resolved","description":"withdraw credits the unsynced incoming delta to totalReceived before safeTransfer, but updates lastSynced only after the external transfer. A receipt callback can invoke permissionless sync while the old baseline remains visible and credit the remaining balance a second time. The outer withdrawal must be authorized, but its recipient callback needs no withdrawal authority. This corrupts the cumulative receipt record; totalReceived is not used for live reserve valuation, so the defect does not itself inflate backing or transfer extra funds. It requires a callback-capable token/recipient combination rather than the normal callback-free shipped token.","line":249,"path":"src/Treasury.sol","reproduction":"Create a fresh Treasury and a token whose transfer first moves balances and then invokes the recipient callback. Start with lastSynced[token] = totalReceived[token] = 0 and transfer or mint 100e18 tokens to Treasury. As APPROVED_OPERATOR, call withdraw(token, recipient, 40e18), where recipient calls treasury.sync(token) in its receipt callback. The outer call first credits 100e18. The callback sees the remaining 60e18 balance against baseline zero and credits another 60e18. Actual final totalReceived[token] is 160e18, while lastSynced[token] and custody are 60e18. Expected totalReceived[token] is 100e18. The earlier review reports test_regression_unsyncedWithdrawalCallbackMustNotDoubleCredit failing with 160e18 != 100e18; its token and recipient fixtures are preserved in artifacts/audit.md.","severity":"low","snippet":"        if (before > counted) {\n            uint256 credited = before - counted;\n            totalReceived[token] += credited;\n            emit Received(token, credited, totalReceived[token]);\n        }\n        token.safeTransfer(to, amount);","title":"Unsynced-receipt fix permits callback double-crediting"},{"citation":"resolved","description":"The USD reader accepts timestamps in the future: its range check allows them, and _tooOld at line 72 returns false until block.timestamp passes the reported time plus the maximum age. Separately, abi.decode narrows the discarded round identifiers to uint80 before the defensive checks, so malformed round-word padding can revert instead of producing the documented zero/stale result. These are defensive validation defects at a fixed trusted dependency. The earlier tests locally supplied abnormal responses and did not establish that an unprivileged caller can cause the pinned Chainlink deployment to emit them.","line":79,"path":"src/UsdPriceFeed.sol","reproduction":"A: Set block.timestamp to 1000000 and keep the primary feed fresh at 0.001e18. Have the USD aggregator return (uint80(10), int256(4000e8), uint256(32536000), uint256(32536000), uint80(10)) and decimals() = 8. Actual: isStale() returns false and latestValue() returns 4e18. Advance 364 days without changing the USD answer and keep the primary fresh: the frozen answer is still accepted despite the one-day maximum age. Expected: a future-dated observation is unusable/stale immediately. B: Have latestRoundData() successfully return the 160 bytes abi.encode(uint256(1)<<80, int256(2000e8), block.timestamp, block.timestamp, uint256(1)<<80), with decimals() = 8 and a valid primary. Actual: latestValue() reverts decoding the invalid uint80 round words. Expected: malformed USD return data produces (0,0) and stale status. The earlier review reports test_regression_futureUsdTimestampMustBeStale and test_regression_badRoundPaddingMustReturnZero failing; their source is in artifacts/audit.md.","severity":"low","snippet":"        (, int256 answer,, uint256 at,) = abi.decode(data, (uint80, int256, uint256, uint256, uint80));\n        if (answer <= 0 || at == 0 || at > type(uint64).max) return (0, 0, 0);","title":"USD-leg reader does not consistently reject malformed oracle observations"}],"hash":"698403726ad6d0a1721d251f9c65791c3f2a90d932690f955d6eecbb0e20ca35","nodeId":"07197594-4e5d-4e11-ae3f-1191664ffa38","outcome":"completed","summary":"Created [.imd-findings.json](/home/seat/.identitymd/work/da7d5b1c-b574-4738-b241-8c03aff321cd/07197594-4e5d-4e11-ae3f-1191664ffa38/.imd-findings.json) with the earlier review’s five findings: three medium and two low.\n\nValidated JSON structure and exact source locations. No other files changed.","treeHash":null,"usage":{"cachedInputTokens":2260608,"inputTokens":171180,"model":"gpt-6-astra","outputTokens":25287,"runtime":"codex","turns":10,"wallClockMs":1017580}}],"verification":[]}