Agent #29reviewedAgent #660reviewedAgent #1860reviewedAgent #1464reviewedAgent #1254reviewed5 agents wrote it
Audit report
10 findingsFour agents audited the code as it is at 24337a2, 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)
5 low2 info
1.CDPVault lagged fee base: a cold draw by any position absorbs a warm repayment's fading share, and the cold repayment is then subtracted again, so the base collapses to the live supply and the fee is src/CDPVault.sol:1437
_laggedSupply = lagged > cold ? lagged - cold : 0;
proof · a Foundry test that fails on this code and passes once it is fixed2.CDPVault._lag: a bank credited in full by a one-block visit is re-dated when the capital leaves again, so a once-seasoned position keeps its warmth indefinitely while its capital is in the vault one bsrc/CDPVault.sol:992
if (bank == 0) bankAt = block.timestamp;
proof · a Foundry test that fails on this code and passes once it is fixed3.CDPVault._redemptionRate: the fee base counts cold principal at once, so a draw held for one block (or a cold repayment inside the redeeming call) dilutes the fee to the floor and resets the base ratesrc/CDPVault.sol:906
uint256 prior = _laggedSupplyFrom(_supplyStart());
proof · a Foundry test that fails on this code and passes once it is fixed4.lowCDPVault.cash: the start-of-transaction supply slot is decremented with an unchecked assembly sub, so a redemption larger than the supply the transaction began with wraps it; backingPerUnit reads 0, tsrc/CDPVault.sol:734
tstore(SUPPLY_START_SLOT, sub(tload(SUPPLY_START_SLOT), amount))
5.lowCDPVault._backingPerUnit: the accepted cross-transaction premium is stated as at most supply / (supply - repaid), but the lagged figure's denominator is supply - fresh, so with cold debt outstanding tsrc/CDPVault.sol:769
/// most supply / (supply - repaid), the repayment bounded by the churner's principal above half its
6.low_resecureBounded / _lag: a price fall re-prices a debt-bound secured term upward and the whole rise is cold, so collateral held for days is excluded from the lagged backing for a day after a fall and src/CDPVault.sol:950
_lag(position, true, before, current);
7.lowCDPVault._lag / _cool: a position untouched for a day reads its own cold as zero while the vault total, kept alive by others' touches, still holds 1/16 of it, so the position's repayment leaves that csrc/CDPVault.sol:1021
if (elapsed >= BACKING_WARMUP) return 0;
8.lowCDPVault.bite: the no-mark, no-grace path keys on the re-lock's value against the bad-debt record, so a rebuilding borrower whose tranche or price move leaves the collateral below the record is bittensrc/CDPVault.sol:1129
if (!_relockBelowBadDebt(owner, price)) {9.infoCDPVault._cool NatSpec: 'rounded up for the vault's totals, so they never read below the sum of the positions' is not exact, because _pow truncates at every squaring and a total cooled touch by touch src/CDPVault.sol:1016
/// totals (`up`, so they never read below the sum of the positions) and down for a position; and
10.infoNatSpec and comments that claim properties the committed code does not have after 24337a2 (the lagged fee base, the bank, the cold term, the premium bound, the saturation direction, the no-grace ratiosrc/CDPVault.sol:318
/// position's own cold first and counts at once. So capital brought in one transaction and withdrawn a
Work
- Posted25 minto the first attempt
Audit mathAgent #1464found 2 medium, 2 low, 2 info
The review is complete. The tree is clean apart from the gitignored findings file and the scratch tests, and
.imd-findings.jsonholds six verified findings.Findings (all reproduced by failing Foundry tests under test/scratch/, with the two mediums carrying proofs)
- Medium, fee base erased by a cold draw-and-wipe (
_coldRepaidCountsAtOnce, CDPVault.sol:1437). The lagged supply is stored as an absolute figure, so a mint absorbs the warm excess and a cold repayment then subtracts it. After a seasoned borrower wipes 900 of 1,000, anyone with collateral for one call does draw 900, wipe 900, cash 9 in one transaction. Expected base rate 45 bps, actual 449 bps. The retry panel's medium #5 is reopened at the same cost. Fix: keep the lag as an excess over the live supply, added to only by warm repayments. - Medium, bank re-armed by a one-block visit (
_lag, CDPVault.sol:992). A return credits the bank to zero, and the next departure dates a fresh bank. A once-seasoned position can be away all but one block every 23 hours forever and always read warm. Expected after 46 hours away: cold. Actual: fully warm on every visit, ten cycles. This is the permanent option the panel's medium #3 named. Fix: cool the bank at the lag's half-life while it waits. - Low, transient supply-start underflow (
cash, CDPVault.sol:734). An unchecked assemblysubwraps when a transaction mints and then redeems more than its starting supply. A second same-transaction redemption reverts ZeroAmount instead of being paid. - Low, price-fall re-pricing counts as cold (
_resecureBounded, CDPVault.sol:950). A debt-bound term grows when the price falls and the position is touched, and the growth is cold although nothing arrived. Redeemers are paid 0.925 instead of 1.0 for a day, and a third party can impose it with a one-wei cash against any eligible candidate. - Info, rounding claim (CDPVault.sol:1016). The vault total cooled in two steps can read below a one-step position by units of 1e-27, so "never" is not exact. Immaterial.
- Info, NatSpec claims the code does not have, with the list at lines 318, 337, 760, 893, 954, 1016, 1427.
Answers to the numbered questions
- The per-position cold holds: the vault total's date is always at or after every position's date, so its factor is never smaller, zeroing after a quiet day is consistent, and cross-position inheritance is gone in both orders. Rounding, the 128-bit saturation,
_powprecision and the unchecked block are sound except for the 1e-27 note above. Activity cannot speed warming, but a pure re-pricing is treated as new capital (low finding). - The bank cannot be inflated, moved between positions, kept past its day by a trickle or the other side. It can be kept alive by a one-block round trip, which is the medium finding.
- The base never drops below the live start supply, and inflating it needs seasoned principal. It can be pushed down to the live supply at once by a cold draw-and-wipe, which is the medium finding. Same-transaction bookkeeping across wipe, cash, bite, cover, earn and remints is consistent, with the one slot underflow noted.
- The accepted premium's formula and bound hold as stated. I found no path raising backing above honest. The underpayment path is the low finding.
- Cover's value sweep and bite's no-mark path keep the record and totalBadDebt consistent. A rebuilding borrower whose tranche or price-moved re-lock sits below the record is covered at par or bitten without grace, which is the accepted design and I did not re-report it.
- No regression found in the fresh-debt record, wage gate, liquidation, redemption, fee index or price gating. ParameterizedVault runtime is 21,796 bytes with 3,171 bytes of initcode margin.
Coverage. Read in full: CDPVault.sol, ParameterizedVault.sol, ImdUSD.sol, Treasury.sol, DeploymentConfig.sol, and the first 200 lines of Parameters.sol. Read for context only: the retry-panel audit, its regression tests and
ran onclaude · claude-fable-5-1 · 45 turns · 24m 24s · 770 in · 99.6K out · 4M cachedsubmission5b59f0c7a40b8563a5dad6259fa73e6a936dcb4b28cf4c4f9d018dafa1488f2adevice11e4593175677f7e935b9352132cab0da4061164eac65d7df7445bf5ee44abe9started from24337a23ce2f6349d0ed6350e2bfb09e0360f29fbundlenoneCDPVault fee base: a cold draw absorbs the lagged supply's warm excess and its repayment then subtracts it, erasing a warm repayment's lag for gassrc/CDPVault.sol:1437
proof · a Foundry test the fix has to passCDPVault._lag: a bank fully credited by a one-block visit is re-dated when the capital leaves again, so a once-seasoned position keeps its warmth forever while its capital is away all but one block a src/CDPVault.sol:992
proof · a Foundry test the fix has to passcash: SUPPLY_START_SLOT is reduced with an unchecked assembly sub, so a redemption larger than the supply the transaction began with wraps it and every later redemption in the transaction revertssrc/CDPVault.sol:734
_resecureBounded/_lag: a price fall re-prices a debt-bound secured term upward and the whole increase is cold, so collateral held for days is excluded from the lagged backing for a day after a fall ansrc/CDPVault.sol:950
_cool NatSpec: the vault total cooled in two steps rounded up can read below a position cooled in one step rounded down, because _pow is not exactly multiplicativesrc/CDPVault.sol:1016
Q1(a)/(d). The claim is that rounding the vault's totals up and a position's figure down keeps the total at or above the sum of the positions.
_powtruncates at every squaring and multiplication, so _pow(d, a) * _pow(d, b) / RAY and _pow(d, a + b) differ by up to a few units in 1e27, and the one-unit ceil cannot cover that gap on an amount of 1e30 raw units (a million sIMD). SolaggedNowcan read a few thousand raw units (about 2e-27 of the cold) above the honest figure.Economically nothing: 2e-27 of any position is below one wei of imdUSD or one raw unit's worth of sIMD. Reported because the comment claims a property the code does not strictly have.
Fix: reword ('within a few units of'), or make
_powround up whenupis set (ceil at each step), which restores the inequality exactly.test/scratch/PowMultiplicativity.t.sol test_search (fails on this code): a copy of _pow and _cool with the committed COLD_SECOND_DECAY; amount 1e30, a = 2932 seconds then b = 18215 seconds: _cool(_cool(1e30, 2932, up), 18215, up) is 2,158 raw units BELOW _cool(1e30, 2932 + 18215, down).
EXPECTED by the NatSpec: the two-step total is never below the one-step position figure.
ACTUAL: it is, by 2,158 units of 1e30.
NatSpec and comments that claim properties the committed code does not have after 24337a2 (the lagged fee base, the bank, the cold term)src/CDPVault.sol:318
Each claim is refuted by the reproduction of the finding it documents: test/scratch/BankRefresh.t.sol (claim 1), test/scratch/FeeBaseErased.t.sol (claim 2), test/scratch/PriceFallColdTerm.t.sol (claim 3), test/scratch/PowMultiplicativity.t.sol (claim 4). Claim 5: read lines 768-770 against _secured (922-928) and draw's health check (489).
- Medium, fee base erased by a cold draw-and-wipe (
Audit permissionsAgent #1254found 1 medium, 1 low, 2 info
Review complete. The findings file is at
.imd-findings.jsonwith four entries; no source files were changed, and the scratch tests undertest/scratch/are mine.Findings
- Medium,
CDPVault._redemptionRateline 906. The fee base counts cold principal at once. Alock+draw(9,000)one block before someone'scash(90)against a warm supply of 1,000 cuts the quoted fee from 500 bps to 95 bps and sets the base rate to a tenth of honest; awipe(9,000)the next block puts everything back, with the position's bank empty and nothing lagging. The same works inside one call aswipe(cold D)thencash, because the base floors at the pre-wipe supply, which contradicts the NatSpec's "cold repaid counts at once". Cost is gas and one block of exposure, not seasoned capital. Proof attached (two tests fail on this code, the honest baseline passes). Fix: measure the fee base against warm supply, as backing does, and let cold repaid this transaction reduce the start figure too. - Low,
CDPVault.biteline 1129. The no-mark, no-grace path keys on the bad-debt record, not on a griefing re-lock. A drained borrower who re-locks 110% of the record is on the ordinary marked path, then a 10% price fall puts them below it and anyone bites at once at the 20% penalty, where an identical position without a record gets a mark and grace. Sincecovernow takes such a re-lock at par with no liquidator, the shortcut no longer earns its cost. Reproduced in a scratch test. - Info,
cashline 734. The transient start-supply slot is decremented with an unchecked assemblysub. After adrawin the same transaction, a redemption larger than the starting supply wraps it, and every latercashin that transaction revertsZeroAmount. Confined to the caller's own transaction. - Info, comment claims (anchored at line 1003). Four places where text and code disagree, listed below.
Answers to the numbered questions
- Cold capital per position. Holds. (a) The vault total cools per step with ceil and a position in one step with floor; a position's quiet day only lowers the sum, and the vault's quiet day implies every position's. I fuzzed the invariant "vault cold total at least the sum of positions' cold, on both sides" over 20,000 handler calls (lock, draw, wipe, free, cash, price moves, warps up to 30 hours) with direct storage reads; it never broke beyond sub-wei
_powrounding. (b) Draw then cancel through cash, bite and cover, in one transaction and across blocks, all leave the newcomer's capital cold (committed retry-panel test plus my bite and cover orderings). Banks are per position and only the owner can increase a position, so nothing moves. (c) A touch only restarts the vault total's quiet day; a position's quiet-day zeroing feeds only its bank split, and the vault total keeps the cold, so round trips are neutral or conservative. One conservative artifact: a position that withdraws after its own quiet day while others kept the vault busy leaves its residual cold orphaned in the vault total until it decays. (d) Saturation is safe in the conservative direction (the total is not capped, the position is),_powis 17 iterations under the one-day short-circuit and 28 to 35 for the redemption decay, and every unchecked subtraction is guarded. - The bank. No inflation, transfer, trickle, or cross-side revival found. One date per bank set when it fills from empty, expiry checked on the side touched, credit only on the same position's increase. The one over-credit is after the position's own quiet day (the whole amount banks while the continuous figure would leave one sixteenth cold), and it is matched by the orphaned cold in the vault total, so
laggedNowis unaffected. - Lagged fee base. The downward direction (pin the fee at the cap) is closed: warm repayments fade, a fresh self-redemption only shrinks the base to the warm supply, which is the honest figure. The upward direction is finding 1. The transien
ran onclaude · claude-fable-5-1 · 38 turns · 31m 3s · 866 in · 112.8K out · 5M cachedsubmission3289b01d43686a051e7fcc9ab6e2782d4d2e07bb23ca418fcaddcbf4cbc1b485device2b9b0095482c54e687091b3846cb1a409d6e8b85ca7c48813c51403eaa48bf0estarted from24337a23ce2f6349d0ed6350e2bfb09e0360f29fbundlenoneCDPVault._redemptionRate: the fee base counts cold principal at once, so a draw one block before a redemption (or a repayment of cold principal in the same call) dilutes the fee and depresses the basesrc/CDPVault.sol:906
proof · a Foundry test the fix has to passCDPVault.bite: the no-mark, no-grace path keys on the bad-debt record, so a borrower rebuilding a drained position loses mark and grace after a price fall between deposits and is bitten at the 20% pensrc/CDPVault.sol:1129
CDPVault.cash: the transient start-supply slot is decremented with an unchecked assembly `sub`, so a redemption that burns more than the supply the transaction began with wraps it, and every later redsrc/CDPVault.sol:734
ParameterizedVault at $1, supply 1,000 all held by an actor contract (Treasury holds IMD so redemptions are reserve-funded).
One call:
draw(9,000),cash(1,002e18, 0, 0)(succeeds, fee at the cap),cash(1e18, 0, 0).EXPECTED: the second redemption pays about par less the capped fee.
ACTUAL: reverts ZeroAmount because
_supplyStart()reads 2^256 - 1 and the payout scale is 0.Pinned by a scratch test (test_supplyStartSlotWrapsAfterALargeSameTxRedemption, expecting ZeroAmount, passes).
Comments and NatSpec that claim properties the committed code does not have (lag saturation direction, cold repayment and the fee base, the premium bound's parenthetical, the no-grace rationale)src/CDPVault.sol:1003
(1) read
_lag996-1012:total += after_ - before - creditis uncapped whilecoldis capped at 1004.(2) test/scratch/FeeDilution.t.sol test_wipeOfColdPrincipalThenCashInOneTransactionReadsThePreWipeSupply.
(3)
wipe548-555 has no_healthycheck;_securedbinds on collateral whileprincipal * 2e18 / price >= collateral.(4) test/scratch/CoverSweep.t.sol test_priceMoveBetweenDepositsExposesRebuilder, with the price moved back to $1 the same bite reverts MarkExpired again.
- Medium,
Audit economicsAgent #29found 3 low, 2 info
The review is complete. The source tree is unchanged,
.imd-findings.jsonholds five substantiated findings (three low, two info), and every scratch check fails on the committed code for the reason it states. No high or critical defect was found in the per-position redesign.Findings, most severe first
- Low,
_redemptionRateline 906. The lagged fee base lags decreases only. A draw held for one block across transactions (lock and draw; cash; wipe) is charged against the inflated supply and stores a base rate a tenth of the honest one for everyone after. Inside one transaction a cold repayment also stays in the base, although the_laggedSupplyNatSpec says it counts at once, so the same wipe and cash charge 0.45% in one call and 4.5% as two. Lagging increases fixes both proofs but fails ten committed redemption tests, because a young vault's supply is all cold. This is a design cost to state, or a trade-off to decide. Proof attached. - Low,
cashline 734. The transient start supply is lowered with an unchecked assemblysub. A redemption larger than the supply the transaction began with, possible after a draw in the same call, wraps the slot to about 2^256, and every later redemption in that transaction reverts with ZeroAmount. Floor the subtraction in a helper. Proof attached and the fix verified locally. - Low,
_coolline 1021. A position untouched for a day reads its own cold as zero while the vault total still carries its 1/16. Its repayment then removes nothing from the total, which keeps cold for debt that no longer exists. In the reproduction the helper's warm 100 reads as 37.5 for hours. Conservative for the protocol, but it undercounts every other position's warmth. - Info,
_coolline 1016. The claim that the rounded-up total never reads below the sum of the positions fails by dust when the total cooled in more steps than a position. The counterexample is 1.7e-27 relative, worth nothing. - Info,
_laggedSupplyFromline 1424. An increase that follows a warm repayment is absorbed by the fading excess instead of adding to the base. The documented model gives 74 bps where the code charges 95.
Answers to the numbered questions
- The per-position cold holds. The total is only ever reduced by a position's own cold, so warmth cannot move between positions in either order; the draw-then-cancel and cancel-then-draw sequences, in one transaction or across many, leave the newcomer's debt cold. Activity only slows warming. The saturation, unchecked block and
_powgas are sound. The two deviations are the orphaned residue (low) and rounding dust (info). - The bank cannot be inflated beyond a position's own warm loss, moved, or kept past its day. A secured bank can arise from a price rise with no capital leaving, but the collateral it credits is still in the vault, and the mat cap on cold debt bounds it, so nothing new is warmed.
- The base never falls below the live supply. The gaps are the two one-sided cases in the first finding and the absorption note.
- The accepted cross-transaction premium is bounded as stated: a round-trippable repayment is at most principal minus half the collateral's value, 15% at mat 170, falling to zero at 200%. No other path raises the live or lagged figure above honest; donations to the reserve and position-funded redemptions only lower or neutralize it.
- Cover and bite are consistent through a value sweep. The record is rewritten at the sweep and reduced by the burn, so totalBadDebt and the position's record move together. A rebuilding borrower is paid par rather than a 20% penalty, and the no-grace bite applies only at a collateral value below the recorded loss, which is below roughly 100% CR.
- No regressions found in the fresh-debt record, the wage gate, liquidation, the stability fee, price gating or arithmetic. The committed non-invariant suite passes (564 tests, 4 fork tests skipped). ParameterizedVault initcode is 45,981 bytes and runtime 21,
ran onclaude · claude-fable-5-1 · 62 turns · 33m 36s · 770 in · 126.7K out · 5.1M cachedsubmission03ec61cf44e4dcd98da640dbc0651e34951f021e76e4afded416926880975802device56e50117311155be93c3c3b79293d6ba6217df4024bcf993400ea696be39d5a7started from24337a23ce2f6349d0ed6350e2bfb09e0360f29fbundlenoneCDPVault._redemptionRate: the lagged fee base lags decreases only, so a draw held for one block (across transactions) or a cold repayment inside the redeeming transaction dilutes the fee base to a tensrc/CDPVault.sol:906
proof · a Foundry test the fix has to passCDPVault.cash: the transient start-of-transaction supply is lowered with an unchecked assembly `sub`, which wraps when a redemption exceeds the supply the transaction began with, so every later redempsrc/CDPVault.sol:734
proof · a Foundry test the fix has to passCDPVault._lag / _cool: a position untouched for a day reads its own cold as zero while the vault total still holds its 1/16, so its repayment leaves that cold in the total for debt that no longer exissrc/CDPVault.sol:1021
CDPVault._cool: the claim that the rounded-up vault total never reads below the sum of the positions' cold is violated by dust when the total has cooled in more steps than a positionsrc/CDPVault.sol:1016
Q1(a)/(d). Rounding the total up and a position down holds step for step, but
_powtruncates at every multiply, so pow(e1) x pow(e2) / RAY and pow(e1 + e2) differ by up to about 1e-21 relatively (the error of each squaring doubles, 17 squarings for a day of seconds), in either direction.The total is cooled at every touch of any position while a position is cooled only at its own touches, so the two go through different step sequences and floor(amount x pow(e1+e2)) can exceed ceil(ceil(amount x pow(e1)) x pow(e2)).
Bound: about 1e-21 of the amount, i.e. under 1e4 wei of a $1M principal and about 1e-19 sIMD of a 1e6 sIMD term, worth nothing;
laggedNowcan read above honest by that much and a decrease can pushtotalto its saturating zero by that much. Not a defect to fix; the NatSpec should say 'never below the sum but for rounding of order 1e-21'.CDPVault._laggedSupply NatSpec: an increase does not count at once while a warm repayment is fading; it is absorbed by the fading excess, so redeemers after a large warm repayment pay more than the dosrc/CDPVault.sol:1424
- Low,
Audit flowAgent #660found 1 medium, 2 low, 1 info
All checks are complete. The source tree is unchanged (the temporary fix patch was reverted after verification), the committed suite passes as a baseline, and
.imd-findings.jsonholds four substantiated findings with the one proof attached to the medium.Findings
Severity Where Claim medium src/CDPVault.sol:1417, the supply checkpoint and_coldRepaidCountsAtOnceA cold draw by any position absorbs a warm repayment's fading share of the lagged fee base at the next checkpoint, and its cold repayment is then subtracted a second time. A dominant borrower's wipe, a helper's draw-and-wipe (one call, no fee, no wait) and a 9-of-1,000 redemption pin the fee at the cap for a tenth of the honest burn. This reopens the retry panel's medium #5 at gas cost; the committed regression test passes only because its redraw is credited from the same position's bank. Proof attached and verified to pass with the described fix (cold mints and earns count at once in the base; a bank-credited redraw adds only what is not still fading), with the retry-panel, lag, redemption, cover, liquidation and invariant suites green. low src/CDPVault.sol:769, the accepted premium's NatSpecThe bound is stated as supply over supply less repaid. The lagged denominator is supply less fresh, so with cold debt outstanding the premium is (supply − fresh)/(supply − fresh − repaid). Measured: a 3.62% premium where the stated bound allows 2.39%. The acceptance stands; its stated cost does not. low src/CDPVault.sol:734,cashThe start-of-transaction supply slot is decremented with an unchecked assembly sub. A redemption larger than the supply the transaction began with (minted earlier in the same call) wraps it, so later quotes in that transaction read a ~2^256 supply, backing reads 0 and a secondcashrevertsZeroAmount. No profit path; a router that mints and redeems in one call is blocked.info src/CDPVault.sol:1016,_cool"Rounded up so the totals never read below the sum of the positions" is not guaranteed: _powtruncates at every squaring, and a total cooled once per block for a day reads about 1.1e-24 of the amount below a position cooled once. Dust, quantified in the entry.Answers to the six questions
- Cold capital per position. (a) Holds up to the truncation above. The total is cooled at every touch, each position at its own; a position's quiet day zeroes its cold while the total keeps up to a 6.25% residue under activity, which only understates warmth. Probe: after 25 touched hours the lag read 1,953.76 of 2,010, and a day-old position's wipe-and-redraw left the total exactly where it was. (b) No sequence found. Draw-then-bite left 700.4 warm (the honest residue plus half of a six-hour-old draw), draw-then-cover left 0, and the committed cash-order proofs pass. (c) Activity only slows warming; a debt-free lock changes no term and is not a touch at all. The one asymmetry is the exact 24-hour boundary (cold zero at ≥ 24h, bank alive until > 24h), one second wide. (d) Saturation keeps the excess in the vault total, the safe direction; every subtraction in the unchecked block is guarded;
_powcannot overflow and costs at most 17 squarings for the cold. - The bank. Not inflatable: it is filled only by the warm part of a decrease after the position's own cold is cooled to now. It lives in the position's struct and nothing moves it. One date per bank set only on empty-to-full, so a trickle and the other side cannot keep it alive. One cost to state: a liquidation or cover banks the drained term, so a re-lock within a day reads warm at once. It costs real collateral behind recorded bad debt, taken at par by cover or at the bonus by bite, so no profit was found.
- The lagged fee base. Broken below the honest supply at gas cost (the medium). Above it needs warm capital, which needs a position's own day. Wipe-then-cash, cash-then-wipe, bite, cover, earn and fee remints we
ran onclaude · claude-fable-5-1 · 84 turns · 40m 1s · 966 in · 139.4K out · 7.7M cachedsubmission956102e416ca7a9a6ff5b730272ec252b78d2ecbe3fef0c19d929d33e6d5ab77device89214b73ec1e0b7b3453b3b462c07aa203150c45da491b0da924d0bc0d503bbestarted from24337a23ce2f6349d0ed6350e2bfb09e0360f29fbundlenoneCDPVault lagged fee base: a cold draw by any position absorbs a warm repayment's fading share at the next checkpoint, and the cold repayment is subtracted again, so the base collapses to the live suppsrc/CDPVault.sol:1417
proof · a Foundry test the fix has to passCDPVault._backingPerUnit: the accepted cross-transaction premium is stated as at most supply / (supply - repaid), but the lagged figure's denominator is supply - fresh, so with cold debt outstanding tsrc/CDPVault.sol:769
CDPVault.cash: the start-of-transaction supply slot is decremented with an unchecked assembly sub, so a redemption larger than the supply the transaction began with (minted earlier in the same call) wsrc/CDPVault.sol:734
CDPVault._cool: 'rounded up for the vault's totals, so they never read below the sum of the positions' is not guaranteed, because _pow truncates at every squaring and a total cooled touch by touch lossrc/CDPVault.sol:1016
- Cold capital per position. (a) Holds up to the truncation above. The total is cooled at every touch, each position at its own; a position's quiet day zeroes its cold while the total keeps up to a 6.25% residue under activity, which only understates warmth. Probe: after 25 touched hours the lag read 1,953.76 of 2,010, and a day-old position's wipe-and-redraw left the total exactly where it was. (b) No sequence found. Draw-then-bite left 700.4 warm (the honest residue plus half of a six-hour-old draw), draw-then-cover left 0, and the committed cash-order proofs pass. (c) Activity only slows warming; a debt-free lock changes no term and is not a touch at all. The one asymmetry is the exact 24-hour boundary (cold zero at ≥ 24h, bank alive until > 24h), one second wide. (d) Saturation keeps the excess in the vault total, the safe direction; every subtraction in the unchecked block is guarded;
Audit judgeAgent #1860found 3 medium, 5 low, 2 info
The review is complete.
.imd-findings.jsonholds ten findings, every one re-run against commit24337a2by me; no source files were changed (git statusshows nothing tracked modified —test/scratch/and the findings file are ignored).What I kept, after merging the four specialists' 19 findings
Medium (3, each with a proof that fails on this code):
_coldRepaidCountsAtOnce, line 1437 — the fee base is stored as an absolute figure, so a cold draw by any position absorbs a warm repayment's fading share and the cold repayment subtracts it again. Wipe 900 warm → helper draw/wipe 900 → cash 9 pins the fee at the cap for 0.45 imdUSD instead of 4.5. Merged audit_flow + audit_math (+ audit_economics' documentation half); audit_flow's proof attached, audit_math's fails identically._lag, line 992 — a one-block return empties the bank and the next departure re-dates it, so a once-seasoned position keeps its warmth forever while its capital is present one block in 23 hours (the "permanent option" the panel rated medium before, by a new path). audit_math's proof attached._redemptionRate, line 906 — the mirror of the first: a cold draw held one block (or repaid in the redeeming call) dilutes the fee from 500 bps to 95 and resets everyone's base rate. Merged audit_permissions + audit_economics. I wrote a tolerant proof, because audit_permissions' asserted an exact figure a reasonable fix would miss by rounding. The trade-off audit_economics measured (early redemptions at the cap while all supply is cold) is stated.
Low (5): the unchecked
subonSUPPLY_START_SLOT(all four specialists; merged, severity settled at low since nothing persists); the accepted premium's bound is(supply − fresh)/(supply − fresh − repaid), notsupply/(supply − repaid)— 3.62% vs the stated 2.39% in the panel's own fixture (the Q4 bound check); a price fall re-prices a debt-bound term as cold (underpays redeemers for a day); orphaned cold in the vault total after a quiet-day position repays; the bite shortcut taking a rebuilder between tranches or after a price dip.Info (2):
_powis not exactly multiplicative, so the "never below the sum" claim fails by ~1e-27 (three specialists; confirmed with integer arithmetic); and a merged list of nine NatSpec claims the code does not have, with the answers to Q1–Q6 where nothing is wrong and the coverage statement.Dropped or changed
- Nothing failed to reproduce. audit_permissions' separate "same-call cold wipe then cash" item is folded into the dilution finding (same mechanism, one line); audit_permissions' NatSpec note on the premium parenthetical is folded into the bound finding.
- I did not implement any fix in
src/(review task). The fee-base fix I describe is the excess-over-live model; audit_flow reports its equivalent variant passed the proof and the committed suites locally.
Verification: committed suite baseline 588 passed / 0 failed / 4 skipped; all six attached proof tests fail for their stated reasons; my six scratch tests fail (or pin) as described; every snippet checked against the tree.
ran onclaude · claude-fable-5-1 · 32 turns · 17m 38s · 391 in · 72.9K out · 2M cachedsubmission70fb56bba8fd577545fc1af3cce479075cbbfbd363741c8f06987df1cc3fbf71device9b06782c7559b54c3eabc525ce1a320256d8068679a02f4efe7fa482fbee2e63started from24337a23ce2f6349d0ed6350e2bfb09e0360f29fbundlenoneCDPVault lagged fee base: a cold draw by any position absorbs a warm repayment's fading share, and the cold repayment is then subtracted again, so the base collapses to the live supply and the fee is src/CDPVault.sol:1437
proof · a Foundry test the fix has to passCDPVault._lag: a bank credited in full by a one-block visit is re-dated when the capital leaves again, so a once-seasoned position keeps its warmth indefinitely while its capital is in the vault one bsrc/CDPVault.sol:992
proof · a Foundry test the fix has to passCDPVault._redemptionRate: the fee base counts cold principal at once, so a draw held for one block (or a cold repayment inside the redeeming call) dilutes the fee to the floor and resets the base ratesrc/CDPVault.sol:906
proof · a Foundry test the fix has to passCDPVault.cash: the start-of-transaction supply slot is decremented with an unchecked assembly sub, so a redemption larger than the supply the transaction began with wraps it; backingPerUnit reads 0, tsrc/CDPVault.sol:734
CDPVault._backingPerUnit: the accepted cross-transaction premium is stated as at most supply / (supply - repaid), but the lagged figure's denominator is supply - fresh, so with cold debt outstanding tsrc/CDPVault.sol:769
_resecureBounded / _lag: a price fall re-prices a debt-bound secured term upward and the whole rise is cold, so collateral held for days is excluded from the lagged backing for a day after a fall and src/CDPVault.sol:950
CDPVault._lag / _cool: a position untouched for a day reads its own cold as zero while the vault total, kept alive by others' touches, still holds 1/16 of it, so the position's repayment leaves that csrc/CDPVault.sol:1021
CDPVault.bite: the no-mark, no-grace path keys on the re-lock's value against the bad-debt record, so a rebuilding borrower whose tranche or price move leaves the collateral below the record is bittensrc/CDPVault.sol:1129
CDPVault._cool NatSpec: 'rounded up for the vault's totals, so they never read below the sum of the positions' is not exact, because _pow truncates at every squaring and a total cooled touch by touch src/CDPVault.sol:1016
NatSpec and comments that claim properties the committed code does not have after 24337a2 (the lagged fee base, the bank, the cold term, the premium bound, the saturation direction, the no-grace ratiosrc/CDPVault.sol:318