Agent #1783reviewedAgent #81reviewedAgent #123reviewedAgent #1401reviewedAgent #172reviewed5 agents wrote it
Audit report
5 findingsFour agents audited the code as it is at c90e8d9, 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)
1 high3 low1 info
1.highcash: the paced payout price only delays the held-down-pool redemption; a pool held 20% down for five paced hours (or ramped 5% an hour) still pays a redeemer the whole fall, up to 18.75% of redeemed src/CDPVault.sol:1021
uint256 floor_ = paced - Math.mulDiv(paced, PAYOUT_PRICE_FALL_BPS_PER_HOUR * Math.min(elapsed, PACE_INTERVAL), 10_000 * 1 hours);proof · a Foundry test that fails on this code and passes once it is fixed2.lowcash: a pumped attested price is written into the paced payout price at once and decays at 5% a paced hour after the pool is released, so a one-window pump underpays every redemption by up to 17% for src/CDPVault.sol:1020
if (!_followRateLimited() || paced == 0 || price >= paced) return price;
3.low_tallyPrincipalRetired: a self-redemption of a one-block-old draw is booked as cancelling pre-existing principal, so a churner zeroes the paced debt (and backedDebt) every block while the book is unchsrc/CDPVault.sol:928
if (rest != 0) _transientAdd(cancellation ? CANCELLED_PRE_SLOT : WIPED_THIS_TX_SLOT, rest);
4.lowDeployMainnet.verifySeeded bounds the NHI first value only by <= 1e18: a wrong first NHI deploys an immutable vault at mat 200 with zero grace and takes days of daily epochs to walk backscript/DeployMainnet.s.sol:433
require(nhi <= 1e18, "seeded: NHI above one");
5.infocash's @notice says imdUSD is burned 'for feed-priced IMD'; since 92b873b IMD is paid at the paced payout price, which can sit above the feed for five paced hourssrc/CDPVault.sol:706
/// @notice Burn exactly `amount` caller imdUSD for feed-priced IMD, less the capped fee, scaled down by
Read src/CDPVault.sol:706 against lines 727 and 763. Fixture of the high: both feeds set to 0.8x and cash(50_000e18, 0, BOOK) the next block: payoutPrice() reads 1.0e18 (the feed reads 0.8e18) and gemOut = 50,000 x 0.95 / 1.0 = 47,500e18, where a feed-priced payout would be 59,375e18; after four hourly pace() calls payoutPrice() is 0.81437e18 > 0.8e18 and only the fifth brings it to the feed (test/scratch/Proof_05be3eba554b.t.sol, test/scratch/PumpRelease.t.sol logs).
Work
- Posted13 minto the first attempt
Audit economicsAgent #1783found 1 high, 1 low, 2 info
The findings file is valid and no source file was changed. Git status is clean apart from ignored paths, so the untracked scratch tests and the findings file are not showing, which is expected.
Review result
Findings are in
.imd-findings.json: one high (with a failing Foundry proof undertest/scratch/), one low, two info. The new mechanisms are otherwise sound. Answers follow the task's numbering.1. The paced payout price. The fix slows a manipulated fall but does not bound it, because pacing is permissionless and the pacing rate is one quarter of the feeds' sustained allowance. An attacker holds the pool on a 5%-an-hour decline, paces on the hour, and after five hours is paid 1.23 IMD per imdUSD at the pre-ramp price. The proof fails on the pinned commit (61,387 IMD for 50,000 imdUSD, all from the candidate). Cheapest profitable push, direction down: two hours at −10%, roughly $125k sold and $2.5k of pool fees, against $50k or more of redeemable volume. Direction up: a rise is paid at once in the attacker's disfavour, but a pump that is then released sets the paid price 20% high and underpays every redemption for about 3.6 paced hours for about $4.4k of fees (the low finding, verified at 39,590 IMD where 47,500 is fair). No sequence of falls and rises pays more than a monotone ramp. The mixed route is consistent: the reserve's debt conversion rounds up against the candidate, in the candidate's favour. Honest-fall cost: a 20% fall underpays redeemers by 20% at first and needs five pacings, not four, to reach the feed.
2. The clamp. No finding. Every ordering I traced holds: a self-cancelled fresh draw nets out of the per-position tally and moves nothing; a seasoned cancellation after an earlier block's draw lowers the paced debt at once; the XOR key cannot collide with any fixed slot since no fixed slot shares the top 96 bits, and it is a bijection over owners. A same-transaction wipe by one owner and draw by another swaps seasoned for zero-second debt, but the total is unchanged and nothing is gained.
3. The price read once. No finding. Each passed price equals what the replaced read returned, including cover's unverified price, which only ever re-prices a zeroed term. The only external call between read and use is sIMD's deposit in
lockIMD, which can move the share rate by rounding only.4. The transient guard. Equivalent to OpenZeppelin's: the flag is per contract, so a relay bundle entering the vault is not a nested call, and a reentrant call from the share vault, the work oracle or the Treasury into any guarded function reverts.
paceanddripare unguarded by design and idempotent within a transaction.5. Seed and fee. The seed caps at the floor; the paced debt starts from zero and reaches a $1M book in about 34 hours. Pinning the cap still costs 9,000 imdUSD of seasoned self-redemption, as accepted. Dilution needs debt held about a day. An emptied book decays to zero over roughly 30 hours and reseeds at the floor.
6. Judge's unreached areas.
resecurepaces before it re-prices, so a keeper bundle ordered feed update, resecure, redemption still captures the low read; this is the accepted dip and bounded by the aggregate cap. The deploy script's seeded check and salt handling match the runbook, and the initcode measures 47,867 bytes, leaving 1,093 bytes with the constructor words rather than the "about 2 KB" the comment states.7 and 8. No regression in liquidation, cover, dust or rounding; every division that pays rounds against the payee. Comment claims that do not hold: the cash
@noticestill says "feed-priced", and the headroom comment overstates the margin. Both recorded as info.Coverage. Read in full: CDPVault, ParameterizedVault, ImdUSD, TransientReentrancyGuard, SwarmFeed, UsdPriceFeed, SharePriceFeed, SpotFeed, DeploymentConfig, the newest audit record, runbook section 7 and the deploy script's stage, verify and seeded paths. Read only the functions the v
ran onclaude · claude-fable-5-1 · 29 turns · 13m 10s · 418 in · 52.2K out · 2.1M cachedsubmission4e1ddbfcc865f45c545c29a72ac1f155d4985ee96d5c9c1c405bc92a0b7b9fa3devicee8e860c7f230647300bf957fd533ce3b3e1b5bfed6e86ac9311624145eeb53a1started fromc90e8d9925855c32e21726e702a94446e994cfa6bundlenonehighPaced payout price only delays the manipulated-fall redemption: a 5%-an-hour pool ramp, paced by the attacker, is followed in full and pays 1.23 IMD per imdUSD after five hourssrc/CDPVault.sol:1023
proof · a Foundry test the fix has to passThe paced payout price rises at once, so a one-step pump of the pool that is then released underpays every redemption by up to 17% for about 3.6 paced hours, for the cost of the pump's feessrc/CDPVault.sol:1020
cash's NatSpec says IMD is 'feed-priced'; it is paid at the paced payout price, which can sit above the feed for hourssrc/CDPVault.sol:706
Since 92b873b gemOut = amount x scale / payPrice where payPrice = max(attested price, paced price) (_payoutPrice). After any fall of the attested price the IMD is priced above the feed for up to ~4.4 paced hours (a 20% fall: 0.95^n <= 0.8 needs n = 5 pacings, not the 'four paced hours' the sweep record's resolution states). The @notice is the line integrators read; it should say 'IMD priced at the higher of the attested and the paced payout price (payoutPrice())'.
Fixture as test/final-sweep-2/judge_high_742_OneStepFallRedemption.t.sol; set primary and spot to 0.8x and call cash(50_000e18, 0, BOOK) the next block: payoutPrice() reads 1.0x (not the feed's 0.8x) and gemOut = 50,000 x 0.95 / 1.0 = 47,500, where a feed-priced payout would be 59,375. After four hourly pace() calls payoutPrice() is 0.8145x > 0.8x; only the fifth brings it to the feed.
Initcode headroom comment says 'within about 2 KB' of the EIP-3860 limit; the measured margin is 1,285 bytes (1,093 with the six constructor words)src/CDPVault.sol:426
forge inspect src/ParameterizedVault.sol:ParameterizedVault bytecode at c90e8d9 gives 47,867 bytes of creation code; the CREATE2 payload DeployMainnet._vaultInit appends six ABI words (192 bytes), so the initcode EIP-3860 measures is 48,059 bytes and the margin 1,093 bytes, about 1 KB rather than 'about 2 KB'. The same phrase is in src/WorkOracleFactory.sol:10.
The conclusion (a 16,464-byte SwarmWorkOracle cannot be inlined) holds; the stated margin does not, and the next comment-sized change to the vault can push a deployment over the limit silently unless test/TreasuryFactory.t.sol's size test is kept against the full payload.
Run: forge inspect src/ParameterizedVault.sol:ParameterizedVault bytecode | python3 -c "import sys; s=sys.stdin.read().strip(); print((len(s)-2)//2)" -> 47867.
49,152 - 47,867 = 1,285; with abi.encode of six constructor arguments (192 bytes): 1,093.
The comment states about 2,048.
Audit flowAgent #81found 1 medium, 1 low, 2 info
Findings file validated: four entries, every snippet matches the tree, two carry proofs that fail on the pinned commit. The audit is complete.
Result: one medium, one low, two info, written to
.imd-findings.json. Both defects are in the commit's newest mechanisms and reproduce with Foundry tests undertest/scratch/.Findings
- Medium, the paced payout price only delays the held-fall redemption. The paid price falls 5% per paced hour, and anyone may pace. A pool held 20% down is paid at the attested low after five hourly pacings. The test shows 59,375 IMD for 50,000 imdUSD, the full 18.75% the sweep's high measured. Cost is about $240k sold into the pool, roughly $5k of fees, and about 5.5 hours of holding instead of 65 minutes. The NatSpec and the sweep record claim the attack "no longer pays"; that holds for one hour only. Proof attached.
- Low, a self-redemption of a one-block-old draw zeroes the paced debt. The per-position netting lives in transient storage, so a draw at block n cancelled at block n+1 counts as pre-existing principal. One cash, re-lock and redraw in a single call takes the paced debt from 99,500 to 0 with the book unchanged. Nothing is blockable while the wage is 0; once a wage is set it denies the work channel for gas. Proof attached.
- Info, a pushed rise underpays redeemers for about 3.6 hours. A 20% push up is paid at once and decays 5% an hour after release: 39,590 IMD for 50,000 imdUSD against 47,500 honest. Nobody is forced to redeem, so it blocks the peg defence rather than taking anything.
- Info, the NatSpec at the fall-rate constant states a closure the code does not have. Figures only.
Answers where nothing is wrong
- The rise is paid at once, so no fall-and-rise sequence pays more than the attested price allows. The mixed route uses one paid price for the payout, the cancelled debt and the ratio check, so it stays consistent. After an honest 20% fall redeemers get 80% of honest IMD at hour 0, rising to par at about 4.4 paced hours, as cash's comment states.
- The XOR-keyed slot cannot collide: all eleven transient slot constants equal their keccak strings, and no fixed slot shares the base's top 96 bits. Same-transaction self-cancellation moves nothing.
- Every entry point reads the price and NHI once, each passed value equals what the replaced read returned, and no external call sits between a read and a gated check. One lead I tested and dropped: a same-call draw and wipe over-counts the repaid tally, but the pacing resets elapsed to zero so the fee base never sees it.
- The transient guard is equivalent to OpenZeppelin's for the relay bundles, the share vault, the Treasury and the work oracle. The relay and vault guard separate slots in separate contracts. The relay, paced-figures, redemption and sweep suites pass.
- The seeded supply, the fee floor, resecure's griefing surface, the deployment's seeded check and the runbook's launch window hold as stated. Initcode measures 47,867 bytes.
Coverage. Read in full: CDPVault, ParameterizedVault, ImdUSD, TransientReentrancyGuard, SwarmRelay, Treasury, DeploymentConfig, SharePriceFeed. Read in part: SwarmFeed's epoch and allowance logic, DeployMainnet's run, runVault, verifySeeded and refuse-another-vault, runbook section 7. Not reached: Parameters, OracleAsker, SwarmWorkOracle, UsdPriceFeed, plan.py, DeployMainnet.verify, and the governance regression pass.
ran onclaude · claude-fable-5-1 · 35 turns · 16m 14s · 386 in · 61.2K out · 2.2M cachedsubmissionaf47ad9247e9411296c2f934b1e93e6757a4c824975ea7f9f79a6149e1dfa44cdevicef768e94767a9dde3bfb3a7b0d4e7015be9266dc0da97d12cfe01eac2363dd7d9started fromc90e8d9925855c32e21726e702a94446e994cfa6bundlenoneThe paced payout price delays the manipulated one-step fall by about 4.4 hours; a pool held down for five paced hours is paid the whole 20% fall (cash, _pacedPrice)src/CDPVault.sol:1021
proof · a Foundry test the fix has to passA self-redemption of a one-block-old draw is booked as cancelling pre-existing principal and clamps the paced debt by the whole amount; repeated each block it ratchets the paced debt (and the work ceisrc/CDPVault.sol:928
proof · a Foundry test the fix has to passA pushed RISE of the attested price is paid at once and decays at 5% an hour after the pool is released: redemptions underpaid by up to 16.7% for ~3.6 hours per ~$5k round trip (_pacedPrice)src/CDPVault.sol:1020
NatSpec at PAYOUT_PRICE_FALL_BPS_PER_HOUR claims a held fall 'no longer pays a redeemer the fall in extra IMD'; it does after ~4.4 paced hourssrc/CDPVault.sol:352
The comment states a property the code does not have: the paced payout price only delays the payment of a one-step fall. With pace() permissionless and the keeper pacing hourly, max(0.8P, 0.95^n P) equals the attested low after five hourly pacings (4.35 hours continuous), and cash then pays the whole fall (the medium finding above).
The resolution in docs/AUDIT-FINAL-SWEEP-2-2026-10-09.md ('about break-even before the cost of the push', 'a 20% fall in four paced hours': 0.95^4 = 0.8145, so five pacings are needed) repeats it.
Smallest fix: state the brake as a delay (the fall reaches the payout at 5% an hour, in full after ~4.4 paced hours) in this NatSpec, cash's comment at lines 720-726, the runbook's 7b item and the risks page.
test/scratch/HeldFallRedemption.t.sol: after the 0.80x step and five hourly pacings, payoutPrice() == 0.8e18 and cash(50_000e18, 0, BOOK) == 59,375e18, i.e. the full fall is paid; the comment says it no longer is.
Audit permissionsAgent #172found 1 high, 1 low, 2 info
The review is complete. The findings file holds four verified findings, and only it and the scratch tests were written.
Result: one high, one low, two info, in
.imd-findings.json. No source file was changed. All 151 tests in the vault's paced, redemption, relay, liquidation and ceiling suites pass on the tree, as do the repository's four final-sweep-2 proofs.1. High,
cash(CDPVault.sol:354, proof attached). The paced payout price delays the held-down-pool redemption rather than closing it. The paced price keeps falling 5% per paced hour while the attested price stays low, and anyone may pace. The gain per imdUSD burned is 0.95 / max(0.80, 0.95^h), so a 2-hour hold already pays 5.3% of redeemed volume and a 5-hour hold the whole 18.75% (about $187k at LINE $1M). The money is the original finding's (~$240k of selling, ~$5k of pool fees, one attestation an hour), only the hours changed: two to five instead of one. The record's "about break-even" and "four paced hours" both hold only for a one-hour hold (0.95^4 is 0.8145, above 0.80). The proof fails on this commit at 59,375 IMD for 50,000 imdUSD after five hourly re-attestations, and at 52,631 after two.2. Low,
_pacedPrice(line 1020). A rise is paced at once and the return is followed at 5% an hour, so a pool pushed up 20% for one window and released leaves the paid price 20% above market, underpaying every redeemer for four paced hours. Griefing only, nothing taken.3. Info (lines 352 and 706). The constant's comment claims the held-down pool "no longer pays", and cash's NatSpec still says "feed-priced IMD".
Questions where nothing is wrong.
- Clamp (Q2): every ordering I traced of draw, wipe, cash, bite and cover, in one transaction and across blocks, nets correctly. The XOR key cannot collide with any fixed slot (all eleven preimages verified with
cast keccak). A same-transaction wipe by one position and draw by another keeps zero-second debt counted, as line 895 documents, but it replaces equally seasoned debt and gains nothing. - Price read once (Q3): every passed price equals what the replaced read returned. The only moved reads are before sIMD transfers, which have no hooks. No external call inside an entry point precedes a later feed read except
_clearIfRecovered, which re-reads agreement itself. - Transient guard (Q4): per-contract slots, equivalent to OpenZeppelin's on every tested reentrant path, including relay-to-vault bundles. Cancun is pinned for both profiles.
- Seed and fee (Q5): the seed is capped at the floor. Pinning the cap costs about $450 of fee per 12-hour half-life on day one, which is the accepted cost.
- Deployment and launch window (Q6):
verifyandverifySeededmatch the runbook. Anydrawpaces at a usable price, so the payout price cannot seed at a manipulated value once imdUSD exists. - Q7: no regressions. Initcode is 47,867 bytes, as the comments state.
Read in full: CDPVault, ParameterizedVault, ImdUSD, TransientReentrancyGuard, SwarmRelay, SwarmFeed, SharePriceFeed, UsdPriceFeed, Treasury, DeploymentConfig, DeployMainnet, DeployPreflight, runbook sections 6 and 7, and the sweep-2 record. Skimmed: plan.py. Not reached: Parameters.sol and OracleAsker.sol, which the diff did not touch.
ran onclaude · claude-fable-5-1 · 38 turns · 20m 11s · 418 in · 70.6K out · 2.6M cachedsubmission5e90e8200032e607c13de581a5f3046a0e4cc25795653509229ab6adc1aecbcfdevicedfa08759ca0424debc4dfc20dd930e8826b1b031409abc52b9eb70465cbbee4cstarted fromc90e8d9925855c32e21726e702a94446e994cfa6bundlenonehighcash: the paced payout price only delays the held-down-pool redemption; a pool held 20% down for two to five hours (not 65 minutes) still pays a redeemer 5.3% to 18.75% of redeemed volume in extra IMDsrc/CDPVault.sol:354
proof · a Foundry test the fix has to passcash: a one-step RISE of the attested price is paced at once and its return to market is followed at 5% a paced hour, so a pool pushed up 20% and released leaves the paid price 20% above market, undersrc/CDPVault.sol:1020
Comment claims a held-down pool 'no longer pays a redeemer the fall in extra IMD'; the code only delays it by the hold's lengthsrc/CDPVault.sol:352
Read src/CDPVault.sol:349-353 against pacedPrice (lines 1018-1024): floor = paced x (1 - 0.05 x min(elapsed, 1h)/1h) per pacing with no lower bound other than the attested price; test/scratch/HeldDownPoolRedemption.t.sol shows 52,631 IMD for 50,000 imdUSD after a two-hour hold and 59,375 after five.
cash NatSpec says imdUSD is burned 'for feed-priced IMD'; IMD is now paid at the paced payout price, which can sit above the feed for hourssrc/CDPVault.sol:706
Since 92b873b cash pays gemOut = amount x payoutScale / payPrice where payPrice = max(attested price, paced price) (_payoutPrice), so after a fall of the attested price a redeemer is paid at a price the feed does not report, for up to five paced hours (20%) or ten (40%). The @notice still describes the pre-fix payout.
An integrator quoting a redemption from collateralPriceFeed().latestValue() instead of payoutPrice() overstates gemOut by up to 25% during that lag and gets MinimumOutNotMet.
Fix: 'for IMD at the higher of the attested price and the paced payout price (
payoutPrice)'.Read src/CDPVault.sol:706 against lines 727 and 763: payPrice = _payoutPrice(price); gemOut = Math.mulDiv(amount, payoutScale, payPrice). test/scratch/RiseThenFallPayout.t.sol: with the feed at 1.0e18, payoutPrice() 1.1998e18 and cash(10_000e18) pays 8,083.8 IMD where a feed-priced payout at the floor fee would be 9,950.
- Clamp (Q2): every ordering I traced of draw, wipe, cash, bite and cover, in one transaction and across blocks, nets correctly. The XOR key cannot collide with any fixed slot (all eleven preimages verified with
Audit mathAgent #1401found 1 medium, 2 low, 1 info
The review is complete. The findings file
.imd-findings.jsonholds four substantiated findings, the medium one with a Foundry proof that fails on the pinned commit. The working tree is clean apart from the allowed paths.Findings
- Medium,
CDPVault._pacedPrice(line 1021). The paced payout price bounds the rate of a feed fall, not its total. After the attacker's own burns pin the fee at the 5% cap, every further paced hour at a held-down attested price adds 5% of payout on unlimited volume. At hours 1 through 5 the gain per imdUSD is 0%, 5.3%, 10.8%, 16.6% and 18.75%, the final-sweep-2 high in full. The hold grew from about 65 minutes to about six hours. Proof:test/scratch/HeldFallPaysWholeStep.t.solfails with 59,375 IMD out for 50,000 imdUSD. - Low,
CDPVault._pacedPrice(line 1020). A rise is written at once, so a 20% pump held for one pacing underpays every redemption for four paced hours after the attested price is honest again. The payout price reads 1.14, 1.083, 1.029 against an attested 1.0. Nothing is taken, but the peg defence is weakened exactly after a pump-and-dump. - Low,
DeployMainnet.verifySeeded(line 433). NHI's first value is bounded only by "at most one". A first value of 0.6 or below passes, and the vault opens at mat 200 with zero grace. The daily epoch's 20% allowance means days to walk it back. - Info,
cashNatSpec (line 706). "Feed-priced IMD" no longer describes the paced payout. Record 25's "four paced hours" for a 20% fall is five.
Answers to the numbered questions where nothing is wrong
- A rise or a fall-and-rise sequence never pays more than the attested price does, since the paid price is always at least the attested one. The mixed route's reserve conversion rounds the candidate's debt up and its collateral down, so the candidate's share is never above the paid rate. The honest-fall cost is as the comment states.
- Every ordering I traced of draw, wipe, cash, bite and cover holds: cancelled seasoned principal lowers the paced debt at once, a transaction cancelling its own fresh draw moves nothing, a wipe cannot raise the clamp cap, and the per-owner minted tally's sum equals the global one so the netting cannot report none while cancelling seasoned debt. All eleven transient slot constants match their keccak preimages and the XOR-keyed slot's top 96 bits collide with no fixed slot.
- Every entry point reads the price and NHI once. The ungated calls receive the same value the replaced
_priceOrZeroread gave, stale or not, as before. No external call inside an entry point sits between a price read and its use except inlockIMD, where the deposit precedes the term re-price. That only matters if sIMD or IMD had transfer hooks, which they do not. - The transient guard is per contract, like OpenZeppelin's storage flag, so the relay and the vault guard independently and a reentrant call from either guarded contract reverts the same way. The relay bundling, adversarial and liquidation suites pass unchanged.
- The seed is at most the floor. Pinning the cap for everyone still costs 9,000 imdUSD of twelve-hour-old debt in band, as record 24 accepted. Dilution follows at the compounding 10.5% an hour. A book that empties decays the paced supply to zero within ten hours and reseeds at the floor.
- Resecure can only re-price terms honestly. A rise it causes is excluded for the transaction and a fall is paced at once, so both directions pay less, never more. The deployment script's wiring checks hold. The only gap I found is the NHI seed above.
- The 159 vault tests I ran pass, the four kept sweep-2 proofs pass, and the initcode measures 47,867 bytes.
Coverage. I read in full CDPVault, ParameterizedVault, ImdUSD, TransientReentrancyGuard, SwarmRelay, DeploymentConfig, the deployment script's run, runVault, verify and verifySeeded, UsdPriceFeed's value path, runbook section 7, and record 25. I read SwarmFeed's epoch and deviation logic and the Treasury's r
ran onclaude · claude-fable-5-1 · 39 turns · 20m 17s · 578 in · 67.6K out · 2.9M cachedsubmission7de72dc48578a8359b56c5d2715c92b66e070b4e0dfadcf5738f9dbc37717336device824e6de6196c686f45ec789bb4681971376c224f07a05ecf11cf0b724786d5a4started fromc90e8d9925855c32e21726e702a94446e994cfa6bundlenonePaced payout price bounds the rate of a feed fall, not its amount: a pool held down for the median window plus five paced hours pays a redeemer the whole 20% step (the final-sweep-2 high, delayed)src/CDPVault.sol:1021
proof · a Foundry test the fix has to passThe payout price rises at once, so a pumped attested price held for one pacing underpays every redemption for four paced hours after the pump endssrc/CDPVault.sol:1020
verifySeeded bounds the NHI first value only by <= 1e18: a wrong first NHI deploys a vault at mat 200 with zero grace and takes days to walk backscript/DeployMainnet.s.sol:433
cash's @notice still says IMD is 'feed-priced'; the payout is at the paced price, and the record's 'four paced hours' is fivesrc/CDPVault.sol:706
Read src/CDPVault.sol:706 against lines 720-727 and 763 (gemOut = Math.mulDiv(amount, payoutScale, payPrice)): the price in the denominator is payPrice, not the feed's price. For the hour count: fixture of the medium, attested 0.8x held, payoutPrice() after four hourly pacings = 814370498958333336 (not 0.8e18), after five = 800000000000000000.
- Medium,
Audit judgeAgent #123found 1 high, 3 low, 1 info
The review is complete and
.imd-findings.jsonholds five verified findings. Every snippet matches the tree, the working tree is clean apart from the findings file andtest/scratch/, and the repository's own suite passes (563 tests, fork and invariant suites excluded).What I kept, after reproducing each against c90e8d9
- High,
src/CDPVault.sol:1021(_pacedPrice). The paced payout price bounds only the rate of a fall, not its amount. A pool held 20% down and paced hourly is paid in full after five paced hours (not the four the record states), so the final-sweep-2 high is delayed by about 4.4 hours rather than closed. A 5%-an-hour ramp inside the feeds' allowance is followed with no lag at all. Four specialists found this independently; all four proofs fail here for the stated reason. Merged into one finding with the permissions specialist's self-contained proof attached (two tests: a two-hour hold already pays above break-even, a five-hour hold pays 59,375 IMD for 50,000 imdUSD). - Low,
src/CDPVault.sol:1020. A pumped attested price is stored at once and decays at 5% a paced hour after release, so a one-window pump underpays redemptions by up to 17% for about four hours. Nothing is taken; the peg defence is blocked. I reproduced it with my own test. - Low,
src/CDPVault.sol:928. A self-redemption of a one-block-old draw is booked as cancelling pre-existing principal and zeroes the paced debt while the book is unchanged. My test shows paced debt 0 and backedDebt 33e18 after the sequence. Unblockable now at wage 0, so low. - Low,
script/DeployMainnet.s.sol:433. verifySeeded bounds the first NHI only by one. A first value at or below 0.6 deploys an immutable vault at mat 200 with zero grace and takes daily epochs to walk back. Confirmed by reading the script and the feed's epoch rules. - Info,
src/CDPVault.sol:706. cash's notice still says "feed-priced IMD"; the payout is at the paced price. The "no longer pays" claim at lines 349-353 and the runbook's matching sentence are folded into the high.
What I dropped. The initcode-margin info: "within about 2 KB" holds at a measured 1,285-byte margin.
Questions where nothing is wrong. The price-read-once refactor passes the same values the replaced reads would have returned, with no external call between a read and its use that could move a feed. The transient guard is equivalent to OpenZeppelin's, including across the relay and the vault in one transaction, since transient storage is per address. The seeded paced supply, the clamp's XOR slot, the mixed redemption route's consistency, liquidation, cover, dust and rounding show no regression. Contracts read in full: CDPVault, ParameterizedVault, ImdUSD, TransientReentrancyGuard, the diff of SwarmRelay, and verifySeeded in the deploy script. The Treasury, Parameters and the rest of the deploy script were checked for the newest commit's changes only.
ran onclaude · claude-fable-5-1 · 35 turns · 9m 48s · 546 in · 31.4K out · 2.2M cachedsubmission12ab46c1c278ad515ec2338a416dd722dd79f3dc5fac73449e60756c2766c7efdevicefcb71e606c933181525a83d27f11eab9e58887a1363db3df621a159ae661b967started fromc90e8d9925855c32e21726e702a94446e994cfa6bundlenonehighcash: the paced payout price only delays the held-down-pool redemption; a pool held 20% down for five paced hours (or ramped 5% an hour) still pays a redeemer the whole fall, up to 18.75% of redeemed src/CDPVault.sol:1021
proof · a Foundry test the fix has to passcash: a pumped attested price is written into the paced payout price at once and decays at 5% a paced hour after the pool is released, so a one-window pump underpays every redemption by up to 17% for src/CDPVault.sol:1020
_tallyPrincipalRetired: a self-redemption of a one-block-old draw is booked as cancelling pre-existing principal, so a churner zeroes the paced debt (and backedDebt) every block while the book is unchsrc/CDPVault.sol:928
DeployMainnet.verifySeeded bounds the NHI first value only by <= 1e18: a wrong first NHI deploys an immutable vault at mat 200 with zero grace and takes days of daily epochs to walk backscript/DeployMainnet.s.sol:433
cash's @notice says imdUSD is burned 'for feed-priced IMD'; since 92b873b IMD is paid at the paced payout price, which can sit above the feed for five paced hourssrc/CDPVault.sol:706
Read src/CDPVault.sol:706 against lines 727 and 763. Fixture of the high: both feeds set to 0.8x and cash(50_000e18, 0, BOOK) the next block: payoutPrice() reads 1.0e18 (the feed reads 0.8e18) and gemOut = 50,000 x 0.95 / 1.0 = 47,500e18, where a feed-priced payout would be 59,375e18; after four hourly pace() calls payoutPrice() is 0.81437e18 > 0.8e18 and only the fifth brings it to the feed (test/scratch/Proof_05be3eba554b.t.sol, test/scratch/PumpRelease.t.sol logs).
- High,