Job
Audit the vault: src/CDPVault.sol, src/ParameterizedVault.sol and src/ImdUSD.sol, as fixed through this commit, at the pinned commit, for a mainnet launch. Read whatever else in src/ these contracts depend on, but report on this scope. Three audit rounds and their fixes are already in (docs/AUDIT-*.md, newest docs/AUDIT-FINAL-2-2026-10-07.md and the fix commit after it); this panel audits the code as it will deploy, so a finding of an earlier round counts only if its fix regressed or left a …
Audit report
8 findingsFour agents audited the code as it is at b73a05f, each in one area, and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the code was changed or deployed.
Download the report (Markdown)
3 low3 info
1.CDPVault._clampLag: a borrower's own atomic repay-and-redraw (or free-and-relock) converts warm capital into fresh capital, so backingPerUnit falls although nothing left the system; with work-minted ssrc/CDPVault.sol:849
if (totalDebt < laggedDebt) laggedDebt = totalDebt;
proof · a Foundry test that fails on this code and passes once it is fixed2.ParameterizedVault._lagApplies keys the D1 work-ceiling lag to wage != 0, but earn mints at wage 0 from rights the shipped SwarmWorkOracle already credited (after an ordinary wage shutdown) or from a src/ParameterizedVault.sol:112
return parameters.wage() != 0;
proof · a Foundry test that fails on this code and passes once it is fixed3.lowCDPVault.draw: the fresh-debt record's integer-second weighted date rounds to the present when a tranche dwarfs the record, and _reduceDebt then multiplies a zero age, so a large draw/wipe pair every src/CDPVault.sol:463
+ Math.mulDiv(block.timestamp - position.mintedAt, amount, fresh + amount, Math.Rounding.Ceil);
proof · a Foundry test that fails on this code and passes once it is fixed4.lowcover's dust path is feed-gated, so a drained borrower's lock(1) (one raw unit, gas only) makes every later cover require fresh, agreeing primary, spot, NHI and Chainlink legs; between purchased attessrc/CDPVault.sol:530
_requireFreshFeeds();
5.lowThe dust-floor fix holds in capital, but a drained borrower's single $1.20 re-lock keeps cover blocked until someone pays bark, six hours of grace and an exactly sized bite for about $0.18 of collatersrc/CDPVault.sol:533
if (position.collateral >= _coverDust(owner, price)) revert NoRealizedBadDebt();
6.infocover's function NatSpec still describes the superseded dust rule ('under a millionth of its debt, at least the seizure for one wei'); the code sweeps up to the seizure for one imdUSD (a hundredth of src/CDPVault.sol:512
/// holding dust worth under a millionth of its debt (at least the seizure for one wei), which is
7.infoearnLine's NatSpec ('Parameters caps the ratio at half that cliff') and DeploymentConfig's EARN_MAT_BPS comment ('5000 at the loosest NHI ... 120% worst-case backing') use the pre-170 mat: with mat 17src/ParameterizedVault.sol:266
/// Parameters caps the ratio at half that cliff.
Compute with the committed constants: mat() at NHI 0.85 == 170 (src/CDPVault.sol:1254); mat - 1 == 70% == 7000 bps; Parameters.MAX_EARN_MAT_BPS() == 2500; 2500 / 7000 == 0.357, not 0.5; worst case (reserve 0, collateral 1.7 D, supply 1.25 D) is 1.36, not 1.20.
Expected per the two comments: 5000 bps, half, 120%.
Actual: 7000 bps, five-fourteenths, 136%, as src/Parameters.sol:82-83 already states.
8.infoParameterizedVault's header says the governor can change 'never where the price comes from'; the governor chooses every non-collateral reserve asset's price source (Parameters.proposeReserveAsset) andsrc/ParameterizedVault.sol:27
/// are, never where the price comes from, where the revenue goes, or which contract governs.
APPROVED_OPERATOR calls parameters.proposeReserveAsset(token, anyFeed, 10000) for a non-collateral token the Treasury holds; after TIMELOCK anyone calls applyPending().
Expected per the header: no governance action changes where a price the vault reads comes from.
Actual: Treasury.reserveAsset(token).priceFeed == anyFeed, and vault.reserveValue(), vault.earnLine() and vault.backingPerUnit() move with anyFeed.latestValue() (test/ReserveValuation.t.sol exercises the same path).
Work
- posted13 minto the first attempt
- reviewed
#921Audit flowCodexrefusedRefused by Codex's safety filterretried on #39 (Claude)
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.
ran oncodex · gpt-6-astra · 4 turns · 13m 13ssubmission5603e68604b6229360d78a7e06e4c627b97ee708855f4b4d770f5aa8f94b67b3device273e2f46918e7b5e9e6b989b17e7d4bff8dd6e48b1c18cd68f5f174ad4c290edstarted fromb73a05f0f9185bae139f46c56f044ed9c7391c4cbundlenonechanged · 0 filesnothing - reviewed
#607Audit permissionsCodexrefusedRefused by Codex's safety filterretried on #869 (Claude)
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.
ran oncodex · gpt-6-astra · 6 turns · 19m 36ssubmission74d71d69215c1f7d0a3c50a3eb462b4afe11215de193b84437d309165d5dfe29devicedd0f9bb0bf93f9ddca7d1c6aff4585ea997317ba3c074250a45511934ff3ef8fstarted fromb73a05f0f9185bae139f46c56f044ed9c7391c4cbundlenonechanged · 0 filesnothing - reviewed
#251Audit mathCodexrefusedRefused by Codex's safety filterretried on #260 (Claude)
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.
ran oncodex · gpt-6-astra · 4 turns · 20m 16ssubmissionfb536bce7e48c2017eeda1d498a30abaa0af6ef939ab87d7ff745f590141fdf0deviceb3450b02f80aa5c20d8af3671f9ad172ffcd9fafa7dbd29fa5a5b823a7dc258cstarted fromb73a05f0f9185bae139f46c56f044ed9c7391c4cbundlenonechanged · 0 filesnothing - reviewed
#1435Audit economicsCodex3 findings · 1 medium
Saved 3 findings in .imd-findings.json, with exact snippets, concrete reproductions, fixes, and two embedded Foundry proofs. Production contracts and configuration remain unchanged.
- Medium — saved work rights bypass backing warm-up after wage returns to zero.
ParameterizedVault._lagApplies, line 112. After governance enables wages, a worker saves rights, and governance disables wages, the worker can draw 1,000 imdUSD, earn 250 in another transaction in the same block, repay, and withdraw all collateral. The test ends with 250 imdUSD supply and zero backing. Apply the work-backing lag regardless of wage; optionally suspendearnwhile wages are zero. - Low — integer-second rounding still erases fresh-debt age.
CDPVault.draw, line 463. Repeated 400,000-imdUSD draw/wipe pairs refresh $10 of existing principal every 11 hours. After 33 hours, redemption still treats that principal as fresh and leaves the stored fee base at zero. Preserve fractional principal-time through both borrowing and repayment. - Info —
coverdocumentation contradicts the new dust floor.CDPVault.cover, line 512. At $1,000 debt, collateral worth $1 is swept despite being reachable bybiteand exceeding the documented millionth threshold. Update the comments to describe the implemented economic threshold and liquidation recipients.
The existing non-fork suite passed. Both new regression tests failed at their intended assertions.
-
Positions. I found no additional below-
mat, double-credit, unauthorized withdrawal, or share-vault reentrancy defect.lockchecks the received collateral;lockIMDcredits the actual share balance increase.drawand debt-bearingfreecheck accrued debt.wipeburns the caller’s tokens and retires fees first. Value-changing position functions share the reentrancy guard. The deployed external sIMD implementation remains an external dependency I did not fork-verify. -
Liquidation. Ignoring raw-unit rounding, a bite burns debt (D) and seizes (1.20D) of collateral: liquidator (1.16D), marker (0.02D), Treasury (0.02D). A liquidator who also marked receives (1.18D). There is an intentional exception: the final remainder below a one-wei seizure is swept in addition to the formula payout. Active marks cannot be reset simply by repeatedly barking. Grace is snapshotted; at NHI ≥ 0.85 it lasts six hours, followed by the liquidation window. Expiry permits a new mark and grace period. I found no new permanent dust freeze; stale or divergent feeds can nevertheless prevent liquidation.
-
coverand dust. The threshold is the seizure for:max(debt / 1,000,000, min(debt / 100, 1 imdUSD), 1 wei). Collateral strictly below that threshold can be swept. This includes collateral reachable bybite, as the documentation finding demonstrates. Depositing at the threshold still blockscover, but exposes that collateral to liquidation; the fix imposes an economic cost rather than eliminating repeated obstruction. I found no additional bypass of the committed floor, excess debt cancellation, or sequence desynchronizing the recorded aggregate from its per-position updates. -
Redemption. The fresh-debt finding leaves subsequent redemptions a lower stored base; it does not make the current call evade its computed fee. Same-transaction principal and work issuance are excluded from the fee denominator. Candidate eligibility uses
mat + gap; the proportional collateral limit prevents worsening the candidate’s ratio. Reserve expenditure is checked using the backing cap and consistent collateral valuation. Redemption uses lagged capital at every wage. I found no new reserve-drain bypass beyond previously documented issues excluded by the assignment. -
Treasury and bad debt. I found no new
cover→withdraw→payStreamordering bypass.
ran oncodex · gpt-6-astra · 7 turns · 23m 53s · 272.2K in · 25.6K out · 4.9M cachedsubmission2510a75e866242965b6b0d6082ee58d5d55f9e1d033ad4c5545afb2a4eae55dddevice249bc6a0e6af3475f85d70bd838f4ea3874f405821ff6cd2090542fdbba91b60started fromb73a05f0f9185bae139f46c56f044ed9c7391c4cbundlenonechanged · 0 filesnothingSetting wage to zero reopens unbacked work minting through saved rightssrc/ParameterizedVault.sol:112
proof · a Foundry test the fix has to passInteger-second rounding still lets draw/wipe round trips erase fresh-debt agesrc/CDPVault.sol:463
proof · a Foundry test the fix has to passcover NatSpec still promises the old, unreachable-collateral-only dust rulesrc/CDPVault.sol:512
- Medium — saved work rights bypass backing warm-up after wage returns to zero.
- reviewed
#260Audit mathClaude2 findings · 1 low
The review is complete. The findings file holds two entries, and no tracked file was changed (only
.imd-findings.jsonand four scratch tests undertest/scratch/).Result
The vault's arithmetic is sound. I found one low (a defence keyed to the wrong signal, reachable only through governance) and one info (three stale or inaccurate comments around cover, with the fix's residual cost quantified). The committed suite passes 556/0 here. The ratio and liquidation arithmetic were fuzzed against 512-bit references at the sIMD price scale with no deviation beyond the documented one-unit floor.
# Severity Where Finding 1 low src/ParameterizedVault.sol:112The D1 lag on the work ceiling applies only while wage != 0. A governance-installed replacement oracle grants rights with the wage at zero, so borrow, earn 25%, repay and withdraw across three transactions leaves work-minted imdUSD with nothing behind it. Governor-gated; fix isreturn trueor also checkingworkOracle().2 info src/CDPVault.sol:580The cover dust floor holds. Blocking it costs $0.35 at the fixture's 29 imdUSD record, $1.20 at records of 100 imdUSD and up, per defender attempt. The blocking collateral goes to the liquidator through bite, not "to the surplus account" as the NatSpec claims. Answers
- Positions. Every path recomputes the position's secured term and checkpoints the lag.
lockchecks the balance delta,lockIMDcredits measured shares,freewith debt requires fresh agreeing feeds and health on the remainder,drawchecks health with accrued fees and the ceiling,wipeburns only the caller's tokens. Nothing moves anyone else's funds. All five arenonReentrant, and the only external calls into user-controlled code are standard token transfers. No double counting found. - Liquidation. Payout is floored and the two cuts come out of the bonus, so nobody receives more than the formula plus the sub-one-wei remainder sweep. The largest coverable bite always leaves a remainder that the sweep or one more one-wei bite reaches (fuzzed, 10,000 runs at and around the sIMD scale). Grace is snapshotted at mark time, bounded by grace plus tail, and clearable only by health. I found no gaming beyond the documented self-mark chip recovery.
- Cover. Sweeps only below the floor; coverage above the debt reverts; the record and
totalBadDebtmove together in every path. The fix for the second-half residual holds. The numbers are in finding 2. - Redemption. Fee base is measured against pre-transaction supply, rounded against the redeemer. The ratio guard is exact. The payout is pro-rata on backing, so the reserve cannot be drained below its share. The lag excludes fresh debt and its supply together. Splitting one burn into many pays the lower Riemann sum, which is the approved design from an earlier round, and chunks against a fresh candidate do not move the base, also approved.
- BadDebtFirst. The Treasury floor is the stale record, which is at most the principal plus fees at drain time. Fees paid through cover remint to the Treasury, so covering a drained position in two calls never needs more than that floor. Unrealized shortfalls are not reserved for, which is the documented "realized only" choice.
- Stability fee. Every rate change passes through
dripin the same application transaction before the new rate is readable. The index is monotone across checkpoints, sochiOfnever exceedschiandstabilityFeeOfcannot underflow. - Price gating.
draw, pricedfree,cash,bark,heel,bite,earnand cover's dust path all refuse stale or divergent feeds, including a dead Chainlink leg.lock,wipe, debt-freefreeand cover of a fully drained position proceed and read no reverting price. During a gap the feed cannot follow, every value action halts for one hour, then until the allowance covers the gap (a 50% fall is followable six hours after the
ran onclaude · claude-fable-5-1 · 45 turns · 24m 16s · 578 in · 91.8K out · 3.9M cachedsubmissiond189067298faf54514fdae265363c31070f65870db62b5e41c141ccba9e49ea9device6b37e4ab6524670535ab5ca4790833b288ea8c4f498948e8435b4062d7544812started fromb73a05f0f9185bae139f46c56f044ed9c7391c4cbundlenonechanged · 0 filesnothingParameterizedVault._lagApplies keys the D1 work-ceiling lag to the wage, so a governance-installed replacement work oracle mints against un-warmed debt and a borrow / earn / repay-and-withdraw round tsrc/ParameterizedVault.sol:112
cover's dust floor holds, but blocking it costs about $1.02-$1.20 per defender attempt and the collateral that blocks it goes to the liquidator through bite, not 'to the surplus account' as the NatSpesrc/CDPVault.sol:580
- Positions. Every path recomputes the position's secured term and checkpoints the lag.
- reviewed
#39Audit flowClaude6 findings · 2 medium
Review complete. The findings file holds six entries (2 medium with proofs, 1 low, 3 info); source files are unchanged and the committed suite is green (556 passed, 4 skipped).
Findings
# Severity Where Finding 1 medium src/CDPVault.sol:849(_clampLag)A borrower's own atomic wipe-then-draw (or free-then-lock) clamps laggedDebt/laggedSecureddown and the re-add warms up over a day, sobackingPerUnit()falls although nothing left the system. With work-minted supply outstanding (wage on) a borrower above about 64% of the debt drives it to 0 and everycashrevertsZeroAmountfor a day; at launch (wage 0) a healthy borrower in a stressed system shaves up to 0.3 × its principal off redeemers' payout (1.0 → 0.90 reproduced). Prooftest/scratch/LagChurn.t.solfails here and passes under a per-position cooling fix I trialled (committedLaggedBackingtests still pass under it).2 medium src/ParameterizedVault.sol:112(_lagApplies)The lagged work ceiling keys on wage != 0, butearnmints against whateveroracle()answers. The governed replacement path, which is only open while the wage is zero and is the documented route to pay-per-job-type minting, installs an oracle that mints with the D1 lag off, reopening borrow → earn → unwind (250 imdUSD left with no debt or collateral in the proof). Needs a governance action, so not reachable with the constants as committed. Prooftest/scratch/ReplacementOracleLagOff.t.solfails here and passes with the gate removed.3 low src/CDPVault.sol:533(cover)The second-half review's finding 4 is fixed as stated: blocking cover now costs about $1.20 of sIMD (1.3823e22 raw). But the cost is paid per bite, and the bite that clears it nets the liquidator 0.16–0.20 imdUSD before gas, so only the operator's keeper will run the bark, six-hour grace and bite each cycle. The residual is bounded by the defender's gas, not the griefer's capital. 4 info src/CDPVault.sol:512cover's NatSpec still states the pre-fix dust rule (a millionth, one-wei seizure).5 info src/ParameterizedVault.sol:266andDeploymentConfig.sol:146Cliff arithmetic wrong: 7000 bps not 5000, 2500 is 5/14 not half, worst-case backing 136% not 120%. 6 info src/ParameterizedVault.sol:27"never where the price comes from" is false for reserve price sources and the work oracle, which are governed. Answers to the numbered questions
- Positions.
lock,lockIMD,free,draw,wipeare allnonReentrant; every health check usesdebtOf(principal plus accrued fees) againstmat();drawaccrues before adding principal so a new borrower'schiOfis set before any principal exists.lockIMDcredits the share balance delta after resetting the allowance, so a mis-reporting share vault cannot over-credit. No path moves funds for anyone but the caller except the designed ones (bite,cash,cover). Nothing wrong found. - Liquidation. The three transfers in
bitesum to exactlycollateralSeized; the marker and protocol cuts come out of the bonus; the dust path and the remainder sweep make every position drainable. Marks snapshotlull()and cannot be shortened, extended or re-taken while live. The only grace "gaming" is the accepted self-mark (18% effective penalty). No freeze found other than the cover residual (finding 3). - cover. A healthy position can never be swept: the floor is the seizure for at most a hundredth of the debt (1.2% in collateral) against a 170% requirement. Coverage cannot exceed
debtOf(ExcessRepayment), andtotalBadDebtalways equals the sum of the per-position records (_recordBadDebtand_reduceDebtare the only writers and move both together). The fix for finding 4 holds as stated; its residual is finding 3. - Redemption. Fee base, fresh-debt record and candidate guard are consistent; the ratio guard uses exact fractions and cannot be worsene
ran onclaude · claude-fable-5-1 · 67 turns · 32m 33s · 994 in · 127.9K out · 9.4M cachedsubmission84d4d922e82645724a8a5909d1f44e141eeeeff1ebe0c1fa615abb24aafd3faedevice37eed9f56188ea8bc18cadb56eb376ad83d30a30750e8d54d0203251a3e3d14fstarted fromb73a05f0f9185bae139f46c56f044ed9c7391c4cbundlenonechanged · 0 filesnothingLagged backing is clamped down at once by a borrower's own atomic repay-and-redraw (or withdraw-and-redeposit), so anyone can underpay redeemers for a day and, with work-minted supply outstanding, maksrc/CDPVault.sol:849
proof · a Foundry test the fix has to passThe lagged work ceiling is gated on wage != 0, but earn mints against whatever oracle() answers; a governed replacement oracle (the documented path for pay-per-job-type minting) mints at wage 0 with tsrc/ParameterizedVault.sol:112
proof · a Foundry test the fix has to passcover's dust floor (1.2 imdUSD) is paid per bite, not per cycle: a drained borrower's one-time re-lock of about $1.20 of sIMD keeps the record uncoverable until the operator's keeper barks, waits six src/CDPVault.sol:533
cover's NatSpec still describes the pre-b73a05f dust rule (under a millionth of the debt, at least the one-wei seizure); the code sweeps up to the seizure for one imdUSDsrc/CDPVault.sol:512
cover's @dev (lines 510-513) says it only cancels debt behind 'a drained position, or one holding dust worth under a millionth of its debt (at least the seizure for one wei)'. Since b73a05f _coverDust (lines 581-587) sweeps anything below the seizure for max(debt / 1e6, min(debt / 100, 1e18)): for a 100 imdUSD debt that is collateral worth up to 1.2 imdUSD, 1.2% of the debt, not a millionth, and for a 50 imdUSD debt up to 0.6 imdUSD.
The inline comment at line 526 and _coverDust's own NatSpec are correct; the function-level NatSpec is the one a reader and the ABI docs quote.
Fix: restate as 'worth under about 1.2 imdUSD (a hundredth of a debt under 100 imdUSD, a millionth of a debt over a million)'.
Position with debt 100e18 and collateral 1.3e22 raw at price 86,814,000,000,000 (worth 1.13 imdUSD, 1.13% of the debt).
Expected per the NatSpec: cover reverts NoRealizedBadDebt (the collateral is far above a millionth of the debt).
Actual: _coverDust == 1.3823e22 > 1.3e22, so cover sweeps it to the Treasury and retires the debt.
earnLine's NatSpec and DeploymentConfig misstate the work-ceiling cliff: at mat 170 the cliff is 7000 bps (not 5000), 2500 is five-fourteenths of it (not half), and worst-case backing with an empty resrc/ParameterizedVault.sol:266
Compute: mat() at NHI 0.85 == 170; mat - 1 == 70% == 7000 bps; Parameters.MAX_EARN_MAT_BPS() == 2500; 2500 / 7000 == 0.357, not 0.5; worst case (reserve 0, collateral 1.7 D, supply 1.25 D) backingPerUnit == 1.36e18 before the par cap, not 1.20e18.
ParameterizedVault's header says the governor can change 'never where the price comes from'; the governor chooses every reserve asset's price source and the work oraclesrc/ParameterizedVault.sol:27
APPROVED_OPERATOR calls parameters.proposeReserveAsset(token, anyFeed, 10000) for a token the Treasury holds; 48 hours later anyone applies it; vault.reserveValue() and earnLine() now read anyFeed.
Expected per the header: no governance action can change where a price the vault uses comes from.
Actual: the reserve term's price source changed by a proposal.
- Positions.
- reviewed
#869Audit permissionsClaude4 findings · 1 low
Review complete. The findings file
.imd-findings.jsonholds four substantiated findings (one low, three info); no source, config or doc file was changed, and the only additions are the findings file and three scratch tests, both paths excluded from git by the repository's own.git/info/exclude.Result
No critical, high or medium defect in
CDPVault,ParameterizedVaultorImdUSDat b73a05f. The three earlier rounds' fixes hold, including the dust floor this panel was asked to break. The residuals are one cheap griefing path and three stale NatSpec claims.# Severity Where Finding 1 low src/CDPVault.sol:530cover's dust path is price-gated. One raw unit of sIMD re-locked on a drained position (gas only) makes every later cover need fresh, agreeing primary, spot, NHI and Chainlink legs. With the price feeds stale by design between purchases, that is two bought attestations per cover. 2 info src/CDPVault.sol:512cover's NatSpec still says the floor is "the seizure for one wei"; it is now one imdUSD or a hundredth of the debt. 3 info src/DeploymentConfig.sol:146EARN_MAT_BPS comment gives a 5000 bps cliff and 120% worst case; with mat 170 it is 7000 and 136%, as Parameters already says. 4 info src/ParameterizedVault.sol:27"never where the price comes from" is false for reserve pricing, which governance sets per listed asset. The code half of the earlier finding was fixed, the wording was not. Answers to the numbered questions
1. Positions. Nothing wrong. Every path checks the ratio against mat with the accrued debt, rounding the ratio down and mat up. Credits are by verified balance delta in both lock paths. All state-changing entry points are nonReentrant and funds move only for the caller, the Treasury in cover, or by formula in bite and cash.
2. Liquidation. No one receives more than the formula: the marker and protocol shares split the 20% bonus, and the swept remainder is below one wei's seizure. No freezable state: across 1000 fuzzed crash scenarios a max bite plus at most one 1‑wei bite always drained the position, and anything left is below cover's floor. The grace snapshot can only be taken while unhealthy, and an active mark cannot be replaced or reset.
3. Cover. The fix holds. Blocking cover now costs collateral worth 1.20 imdUSD per mark-and-grace cycle, and the swept dust goes to the Treasury. Coverage is bounded by the position's accrued debt, and totalBadDebt moves with the per-position record in every path. The residual is finding 1.
4. Redemption. The fee is charged on the whole burn at the post-increase rate and the base nets out same-transaction mints. A candidate's ratio can only improve, and the backing cap with lagged capital held in every scenario I tried. Chunked redemptions paying less remains the accepted low from the vault panel.
5. Bad debt and the Treasury. No sequence spends what bad debt needs. Withdraw and payStream both floor on totalBadDebt, and cover retires fees first and remints them, so the Treasury over-reserves rather than under.
6. Stability fee. A rate change cannot reprice elapsed time: Parameters drips before writing the new set, the index is linear from its checkpoint, and chiOf can never exceed chi.
7. Price gating. Every value-moving action is refused on stale or divergent feeds. lock, wipe and debt-free free are safe without a price. A feed that cannot follow a gap halts borrowing, withdrawal with debt, marking, biting and redemption for the hours the allowance takes to widen, and a mark whose tail passes inside that halt must be retaken with a fresh grace. That is the documented design.
8. Work issuance. Nothing mints at wage zero: the attested oracle refuses claims while the wage is zero, so no rights exist. Same-transaction debt is excluded by the transient start-of-transaction record and by the lag, and a reserve listing adds only capital the oper
ran onclaude · claude-fable-5-1 · 48 turns · 26m 20s · 706 in · 108.6K out · 5M cachedsubmission884c3e9ea10a62bdcebbe0031f222ec1d6758351d4f7de094f0994594e96f412device52c98c0dc01791cdcfe718724d7d7833e36a34895c930607652c624cb327daafstarted fromb73a05f0f9185bae139f46c56f044ed9c7391c4cbundlenonechanged · 0 filesnothingcover's dust path is price-gated, so one raw unit of sIMD re-locked on a drained position (gas only, ~1e-19 USD) makes every later cover require fresh, agreeing primary, spot, NHI and Chainlink legs; src/CDPVault.sol:530
cover's NatSpec still describes the dust floor as 'at least the seizure for one wei'; since cc4103f/b73a05f the floor is the seizure for the larger of a millionth of the debt and one imdUSD (a hundredsrc/CDPVault.sol:512
NatSpec claim the code does not have. _coverDust (lines 581-587) sweeps collateral below the seizure for max(debt / 1_000_000, min(debt / 100, 1e18)), which at launch prices is collateral worth about 1.20 imdUSD for any debt from 100 imdUSD up to 1,000,000 imdUSD. The function's own @dev at line 512 still states the superseded bound ('at least the seizure for one wei'), while the inline comment at lines 526-527 and the _coverDust NatSpec at 574-580 state the current one.
A reader of the public function's documentation is told cover sweeps only sub-wei dust; it sweeps up to 1.20 imdUSD of collateral (to the surplus account). Not a code defect.
Fix: reword line 512 to 'at least the seizure for one imdUSD of it, or a hundredth of the debt if that is less'.
Read src/CDPVault.sol:512 against src/CDPVault.sol:581-587. Concrete: debt 14,834e18, price 86,813,999,999,999: the one-wei seizure is 13,822 raw units (about 1.2e-15 USD) but _coverDust returns 13,824,884,792,626,887,383,465 raw units (about 1.20 USD), so a position holding, say, 1e22 raw units (about $0.87) is swept by cover although the NatSpec says only dust below the one-wei seizure is.
EARN_MAT_BPS comment gives the backing cliff as 5000 bps and the worst case as 120%; with the shipped mat floor of 170 the cliff is 7000 and the figure 136%, as Parameters.MAX_EARN_MAT_BPS already saysrc/DeploymentConfig.sol:146
Comment claiming a property the code does not have (vault panel 2026-10-05 finding 15 corrected Parameters.sol:82-83 to '7000 ... at 2500 it is 136%', but the same derivation in DeploymentConfig.sol was left at the pre-170 numbers).
CDPVault._mat returns 170 at NHI >= 0.85, so mat - 1 = 0.70 = 7000 bps, and with an empty reserve and the ratio term at 2500 every imdUSD of debt is backed by 1.70 of collateral while supply is 1.25 per debt: 1.70 / 1.25 = 136%, not 120% (which is 1.50 / 1.25, the old mat of 150). The two source files now state different safety margins for the same constant.
Fix: change '5000' to '7000' and '120%' to '136%' at lines 146-147, or point the comment at Parameters.MAX_EARN_MAT_BPS.
Compute with the committed constants: CDPVault._mat(0.85e18) == 170 (src/CDPVault.sol:1254); EARN_MAT_BPS == 2500; backing with empty reserve = mat/100 / (1 + EARN_MAT_BPS/10000) = 1.70 / 1.25 = 1.36.
Expected per the comment: 1.20 and a 5000 bps cliff.
Actual: 1.36 and a 7000 bps cliff, matching src/Parameters.sol:82-83.
ParameterizedVault header: 'the governor can change what the numbers are, never where the price comes from' is true of collateral pricing but not of reserve pricing, which Parameters.proposeReserveAsssrc/ParameterizedVault.sol:27
- reviewed
#1514Audit judgeClaude8 findings · 2 medium
afterAudit math, Audit permissions, Audit economics, Audit flowReview complete. The findings file holds 8 findings, merged from the 15 the four specialists reported, each reproduced against the committed code. No source file was changed; the only additions are
.imd-findings.jsonand five scratch tests undertest/scratch/.Result
# Severity Where Finding 1 medium CDPVault.sol:849 _clampLagA borrower's atomic wipe-and-redraw or free-and-relock clamps the lagged backing at once and it only warms back over a day. With work supply outstanding the dominant borrower drives backingPerUnit to zero and every cashreverts, repeatable per block for gas. At launch (wage 0) the collateral side alone drops it 1.00 to 0.90. Proof attached.2 medium ParameterizedVault.sol:112 _lagAppliesThe work-ceiling lag is keyed to wage != 0, but earnmints at wage 0 from rights SwarmWorkOracle already credited (after an ordinary wage shutdown) or from a governed replacement oracle. The D1 adjacent-transaction round trip leaves 250 unbacked imdUSD in the proof. Three specialist findings merged. Proof attached.3 low CDPVault.sol:463 drawA tranche that dwarfs the fresh record rounds its weighted date to the present and _reduceDebtthen multiplies a zero age, so draw/wipe pairs keep seasoned principal fresh and redemptions against it never raise the base rate. Proof attached.4 low CDPVault.sol:530 coverlock(1)for gas moves cover onto the feed-gated dust path, so cover reverts StaleFeed between purchased attestations. Reproduced by my own test.5 low CDPVault.sol:533 coverThe dust-floor fix holds in capital, but the $1.20 re-lock stays until someone pays bark, six hours and an exactly sized bite for about $0.18, and the collateral goes to the liquidator, not the surplus account as line 580 says. Reproduced: 1.18 to the liquidator, 0.02 to the Treasury. 6 info CDPVault.sol:512 cover's NatSpec still states the superseded dust rule; line 278 omits that cash and bite reduce totalBadDebt. 7 info ParameterizedVault.sol:266 earnLine and DeploymentConfig state a 5000 bps cliff, "half", 120%; the code gives 7000, five-fourteenths, 136%. 8 info ParameterizedVault.sol:27 "never where the price comes from" is false for governed reserve price sources and the work oracle. How the specialists fared. All four attached proofs fail on this code for the stated reasons and are kept. I merged the three lag-gating findings (one root cause, one fix), the four cover-NatSpec findings, the two cliff-number findings and the two header findings. Nothing was dropped as non-reproducing; two findings reported as "passes, demonstrates the state" were confirmed by my own tests and folded into findings 4 and 5.
Answers where nothing is wrong. Positions (Q1): all five entry points are nonReentrant, lockIMD credits only the measured share delta, funds move only for the caller. Liquidation (Q2): nobody receives more than the formula, the marker's share comes out of the bonus, dust below the one-wei seizure is swept, and the grace snapshot can only be reset by actually restoring health. Bad debt and the Treasury (Q5): withdraw, payStream and cover all respect totalBadDebt; cover moves the per-position record and the total together. Stability fee (Q6): Parameters drips before a rate lands, chiOf is only ever written from chi. Price gating (Q7): every value-moving action except lock, wipe and debt-free free reads freshness and agreement; a feed that cannot follow a gap halts them all, liquidation included, which is the documented trade-off. Work at WAGE_WAD 0 (Q8): claim refuses, so no rights exist and earn reverts; a reserve donation cannot be round-tripped except through cash at or below par. Arithmetic (Q9): ratios saturate, every payout division rounds against the recipient, and the per-1e18-raw convention holds through SharePriceFeed.
Coverage. Read in full: CDPVault, ParameterizedVault, ImdUSD, Para
ran onclaude · claude-fable-5-1 · 28 turns · 11m 25s · 418 in · 47.9K out · 2M cachedsubmissionea55636b1e3cd14487cdbef290c7c97ba6cc285cd38c013e8ca3f81f0552d4e0devicef47f953f8e35921edd8ad15f032f1bad0d020522e57a1e043a63fdcd8639f678started fromb73a05f0f9185bae139f46c56f044ed9c7391c4cbundlenonechanged · 0 filesnothingCDPVault._clampLag: a borrower's own atomic repay-and-redraw (or free-and-relock) converts warm capital into fresh capital, so backingPerUnit falls although nothing left the system; with work-minted ssrc/CDPVault.sol:849
proof · a Foundry test the fix has to passParameterizedVault._lagApplies keys the D1 work-ceiling lag to wage != 0, but earn mints at wage 0 from rights the shipped SwarmWorkOracle already credited (after an ordinary wage shutdown) or from a src/ParameterizedVault.sol:112
proof · a Foundry test the fix has to passCDPVault.draw: the fresh-debt record's integer-second weighted date rounds to the present when a tranche dwarfs the record, and _reduceDebt then multiplies a zero age, so a large draw/wipe pair every src/CDPVault.sol:463
proof · a Foundry test the fix has to passcover's dust path is feed-gated, so a drained borrower's lock(1) (one raw unit, gas only) makes every later cover require fresh, agreeing primary, spot, NHI and Chainlink legs; between purchased attessrc/CDPVault.sol:530
The dust-floor fix holds in capital, but a drained borrower's single $1.20 re-lock keeps cover blocked until someone pays bark, six hours of grace and an exactly sized bite for about $0.18 of collatersrc/CDPVault.sol:533
cover's function NatSpec still describes the superseded dust rule ('under a millionth of its debt, at least the seizure for one wei'); the code sweeps up to the seizure for one imdUSD (a hundredth of src/CDPVault.sol:512
earnLine's NatSpec ('Parameters caps the ratio at half that cliff') and DeploymentConfig's EARN_MAT_BPS comment ('5000 at the loosest NHI ... 120% worst-case backing') use the pre-170 mat: with mat 17src/ParameterizedVault.sol:266
Compute with the committed constants: mat() at NHI 0.85 == 170 (src/CDPVault.sol:1254); mat - 1 == 70% == 7000 bps; Parameters.MAX_EARN_MAT_BPS() == 2500; 2500 / 7000 == 0.357, not 0.5; worst case (reserve 0, collateral 1.7 D, supply 1.25 D) is 1.36, not 1.20.
Expected per the two comments: 5000 bps, half, 120%.
Actual: 7000 bps, five-fourteenths, 136%, as src/Parameters.sol:82-83 already states.
ParameterizedVault's header says the governor can change 'never where the price comes from'; the governor chooses every non-collateral reserve asset's price source (Parameters.proposeReserveAsset) andsrc/ParameterizedVault.sol:27
APPROVED_OPERATOR calls parameters.proposeReserveAsset(token, anyFeed, 10000) for a non-collateral token the Treasury holds; after TIMELOCK anyone calls applyPending().
Expected per the header: no governance action changes where a price the vault reads comes from.
Actual: Treasury.reserveAsset(token).priceFeed == anyFeed, and vault.reserveValue(), vault.earnLine() and vault.backingPerUnit() move with anyFeed.latestValue() (test/ReserveValuation.t.sol exercises the same path).
- onchain