{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"ed4f7f6d-0b24-4762-91b5-3f1d61657d68","kind":"audit","nodes":[{"acceptedSubmissionHash":"2fef1ea787297a9273c14646b645714f9f882c767cfe7a3a345afef5ec19ef6d","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"04c10f2aad55408fe2ab71f55214b5b308e12ba5886795812de84f7143072d49","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"2ea16728f65e3554e3ba6c5bc91e53dac722ccac905370c9713fd66028eaaa0f","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"3dd7b41e954b758b47368a0031a2517405fafc445368b753fac472ed779dc176","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"c995158ab01d8d15bee5cc8e06309fa836a9013c972b8fd5ffc607c4c90b7ea0","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Audit the whole system: src/, script/DeployMainnet.s.sol, script/DeployPreflight.sol and deploy/mainnet/, at the pinned commit, for a mainnet launch. Sixteen audit rounds and their fixes are in (docs/AUDIT-*.md; the chain is web/content/docs/reference/audit-history.md). This is the LAST review before mainnet: a panel over the whole system at the commit that will deploy. Since the previous whole-system sweep (a3aa9e4, docs/AUDIT-FINAL-SWEEP-2-2026-10-09.md) the vault changed twice and nothing else in src/ did: git diff a3aa9e4 e4baedf -- src script deploy. The sweep's high (a pool held down through the feed's window paid a redeemer the whole fall in IMD) was answered with a paced payout price: cash pays at max(attested, paced), the paced price falling at a bounded rate. A panel on that fix (docs/AUDIT-PAYOUT-VAULT-PANEL-2026-10-09.md) showed the rate bounds the speed of a fall, not its size (at 5% an hour a five-hour hold was paid the whole step); the rate is now 1% an hour, a 20% step reaches the payout after about 22 paced hours, and the fix rests on the claim that a pool cannot be held 20% down on its only market for most of a day at a cost below the gain. The same commits clamp the paced debt against pre-existing principal a transaction cancels, with the position's own principal minted within FRESH_DEBT_WINDOW (12 hours) netted out, seed the paced supply no higher than the fee-base floor, read the price and NHI once per entry point, replace the reentrancy guard with a transient-storage one, and make verifySeeded check the first NHI. Both records' Resolution sections say how each finding was answered and what was ACCEPTED with its bound stated; an accepted item is a finding only if the reason is wrong or the bound does not hold. A finding of an earlier round counts only if its fix regressed or left a gap. Rank severity by what a finding lets someone take or block with the constants as committed (LINE $1M, wage 0, mat 170 at NHI >= 0.85, the backing's rise 2 points of par an hour, the follow 10% an hour, the payout price's fall 1% an hour, PACE_INTERVAL 1 hour, fee floor 100,000 imdUSD, fee cap 5%, FEED_MAX_DEVIATION_BPS 2000, SKEW_BPS 500).\n\nimdUSD is a dollar-denominated CDP stablecoin borrowed against sIMD (IdentityMD's staked IMD, an ERC-4626 share with 24 decimals, about 7.95 IMD each). Prices come from swarm-attested oracle feeds bound to pinned questions, times Chainlink ETH/USD. Everything about the deployment is in src/DeploymentConfig.sol and docs/MAINNET-RUNBOOK.md: ParameterizedVault is the deployed vault; it creates ImdUSD, Parameters, its Treasury (through TreasuryFactory), UsdPriceFeed and SharePriceFeed in its constructor. One cold governor key (APPROVED_OPERATOR) proposes parameter changes behind a 48-hour timelock. Collateral pricing is per 1e18 raw units throughout. IMD's only market is a full-range Uniswap v4 pool, about $2.3M a side with a 1% fee; docs/PARAMETERS-2026-10-05.md has the numbers every economic parameter was chosen from.\n\nAnswer each numbered question, including the ones where nothing is wrong:\n1. THE PAYOUT PRICE'S ECONOMICS (cash, _pacedPrice, _paceWith, payoutPrice). With the pool as it is (about $2.3M a side, 1% fee, no other market) and the feeds as committed (a 20% step from a fresh anchor, 40% after two silent hours, both feeds reading the one pool, the 13-sample two-hour median): the cheapest profitable hold-down in money and hours, now that each paced hour is worth 1% of redeemed volume and the fee cap eats the first five; what dip-buying, arbitrage and the attacker's inventory cost over such a hold, and whether anything in the protocol (the Treasury's fall trigger, resecure, liquidations at the held price) shortens it or pays for it; whether a RISE held through one window then released, or any fall-and-rise sequence, pays a redeemer more than the honest IMD or blocks more than the stated 18 hours; the cost to honest redeemers after a real fall against the NatSpec. Is 1% an hour the right bound, and if not, what is?\n2. THE CLAMP AND THE NETTING (_tallyPrincipalRetired, _recentlyMinted, _clampPacedDebt, CANCELLED_PRE_SLOT, MINTED_BY_SLOT). Every ordering of draw, wipe, cash, bite and cover by one or several positions, in one transaction and across blocks and across the twelve-hour window: can zero-second debt count for the work ceiling sooner than the follow rate, can the netting of a position's recent principal hide the cancellation of seasoned debt (a confederate's, or the same position's older tranche after a redraw moved its record), and can the paced debt be driven below the seasoned book for less than holding the debt twelve hours?\n3. THE PACED FIGURES ACROSS THE SYSTEM. With the Treasury (fundOracle, redeemIMD, cover, withdraw's floor, donations), Parameters (a change mid-flight), the feeds (a first value, a widened epoch, a stale window, a Chainlink outage) and SwarmRelay (bundling a feed update with a liquidation, a pace or a resecure): any sequence that pays a redemption more than the honest backing at the paid price, mints work against debt not held for the follow rate, moves the fee base faster than the follow rate, or desynchronises a record.\n4. THE LAUNCH WINDOW, hour by hour for the first day, docs/MAINNET-RUNBOOK.md section 7 against the code: the first values and verifySeeded (both references, the bands), stage two with VAULT_SALT through a private relay, the keeper (pace() hourly, resecure after each update, bite with its own imdUSD), the first draws, the first redemptions (the fee floor, the seeded supply, the payout price seeded at the first usable price), the first fall and liquidation (grace, dust, bad debt covered with no fees accrued), and every way the protocol can halt that day and how each recovers.\n5. THE ORACLE AS AN ATTACK SURFACE ON THE VAULT beyond question 1: over-borrowing at a pushed-up price then a liquidation or redemption, NHI (mat and grace), the spot's skew, a first value after a silent lifetime; in money and hours at LINE $1M.\n6. GOVERNANCE, THE TREASURY AND THE DEPLOYMENT for regressions only: the timelock and bounds, every exit from the Treasury bounded as documented, the reserve valuation, the factories; DeployMainnet.run, verifySeeded, runVault (VAULT_SALT, _refuseAnotherVault, record before verify), verify, plan.py, the pinned bodies; what can still be deployed wrong and pass. Initcode 47,961 of 49,152 bytes.\n7. Every comment, NatSpec or runbook line in scope that claims a property the code does not have, and the list of what you read in full and what you could not reach.\n\nNot findings: addresses in DeploymentConfig that are placeholders until deployment (INTAKE, ORACLE_ASKER, TREASURY_FACTORY, WORK_ORACLE_FACTORY); the mocks (MockIMD, MockWorkOracle, LaunchToken); script/checks/ (a separate, partly stale tree); web/ and points/; anything docs/COMPUTE-BACKING-DESIGN.md describes as future work; and findings of the earlier audits in docs/AUDIT-*.md and docs/INTERNAL-AUDIT-2026-10-04.md, unless the fix regressed. A constant set to a deliberate economic value is not a finding; an arithmetic or ordering error in how it is used is.\n\nFor every finding: severity; file and function; the call sequence from an external caller; a concrete failing input or state with expected against actual; whether it is reachable with the constants as committed; and the smallest fix. Also report every place a comment or NatSpec claims a property the code does not have, and say which contracts you read in full and which you could not reach.","parentJobId":null,"planHash":"742d8ddae8721e8da49887fc0a88f58a6a92e690ec7d870e3d1fac62d5eacc64","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"ed4f7f6d-0b24-4762-91b5-3f1d61657d68","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52266","feedbackHash":"2e6bdf01c900f875074e91cb51ebbc6ee968da1c81de0fcb19e7472128208164","nodeKey":"audit_economics","submissionHash":"2fef1ea787297a9273c14646b645714f9f882c767cfe7a3a345afef5ec19ef6d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52323","feedbackHash":"a1e1192c0d611541bc9e9fcc71b51449ec0ce1d83023d1b1a69c8736b2e58582","nodeKey":"audit_flow","submissionHash":"04c10f2aad55408fe2ab71f55214b5b308e12ba5886795812de84f7143072d49","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50969","feedbackHash":"4964a8cb497dd3d5f6f5449d4c18e1c11ea17a34895cc6e3072af863b29c2054","nodeKey":"audit_judge","submissionHash":"2ea16728f65e3554e3ba6c5bc91e53dac722ccac905370c9713fd66028eaaa0f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51347","feedbackHash":"65ebc3bfe1f2b9829a0f632ff9fe225e8834c6a97be0ee1d34c1e97e93d969a6","nodeKey":"audit_math","submissionHash":"3dd7b41e954b758b47368a0031a2517405fafc445368b753fac472ed779dc176","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51429","feedbackHash":"1e1de1aa0ec0ae2d5a2778ec0f76b5e258b3619609be166e9d08073ae75d013a","nodeKey":"audit_permissions","submissionHash":"c995158ab01d8d15bee5cc8e06309fa836a9013c972b8fd5ffc607c4c90b7ea0","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"b87d8ebf1c3081e8e3ff90c69ad122c41a61979d45a6be2a8daa19e4705983f4","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3a720671678111bc","findings":[{"citation":"resolved","description":"The payout vault panel's low #3 fix nets a cancelled position's own principal minted within FRESH_DEBT_WINDOW (12 hours) out of CANCELLED_PRE_SLOT on the stated ground that 'the paced debt has had at most that long to follow it' (NatSpec at src/CDPVault.sol:931-935). With the constants as committed the follow is 10% of max(paced debt, 100,000 imdUSD) per paced hour, compounding, so a draw is fully inside the paced debt within hours (a 100,000 draw on a 99,500 book in about 7.3 paced hours; a 10,000 draw in one), long before it is 12 hours old. Any cancellation (cash, bite, cover) of principal between its follow time and 12 hours old is therefore booked nowhere, the clamp does not fire, and a second position's draw one block earlier counts in full for ParameterizedVault.backedDebt at once. This reopens final sweep 2 low #2 (1) ('debt cancelled by a redemption and drawn again by someone else backs nothing until it has been held', CDPVault NatSpec 311-313 and ParameterizedVault 238-241, 265-266) for that window: tx1 Bob draws X; tx2 Alice (debt 1 to 12 hours old, fully followed) self-redeems X against her own position (fee returns to her collateral, so the cost is gas); backedDebt now includes Bob's zero-second X. Only the work ceiling reads backedDebt and WAGE_WAD is 0 at launch, so nothing is takeable or blockable with the constants as committed (low); once a wage is set behind the timelock, 25% (EARN_MAT_BPS) of X of work minting is authorised against debt held for one block, the sweep-panel round trip at one-block holding time. The NatSpec claim at lines 931-935 and the record's resolution #3 ('the paced debt has had at most twelve hours to follow it, so cancelling it is not cancelling seasoned debt') describe a property the code does not have at the committed follow rate. Smallest fix: net out of a cancellation only principal younger than the time the follow needs, which the paced figure cannot know per position; the conservative choice is to book every cancellation of principal older than PACE_INTERVAL (one hour, the longest a draw can go unfollowed by one step) as pre-existing, i.e. replace the FRESH_DEBT_WINDOW test in the netting with a one-interval test (keep `_recentlyMinted` for the fee as it is), accepting that a self-redemption of an hour-old draw ratchets the paced debt by what the follow had absorbed (it is then under the live debt and recovers at the follow rate, the residual the record already accepts).","line":953,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// The fresh-principal netting in _tallyPrincipalRetired (payout vault panel 2026-10-09, low #3) assumes the\n// paced debt \"has had at most twelve hours to follow\" principal younger than FRESH_DEBT_WINDOW. At the\n// committed constants the follow absorbs a draw in hours (10% of max(paced, 100,000) an hour, compounding),\n// so a sibling position's draw that is already fully inside the paced debt, cancelled one block after a\n// second position's draw, is netted out as \"fresh\" and the clamp never fires: the second position's\n// zero-second debt counts in full for ParameterizedVault.backedDebt. This is the final sweep 2 low #2 (1)\n// reopened for cancellations of debt between its follow time and twelve hours old.\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract FnFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 hours;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external view returns (bool) {\n        return block.timestamp - updatedAt > maxAge;\n    }\n}\n\ncontract FnAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract FreshNettingHidesFollowedDebtTest is Test {\n    address private constant BOOK = address(0xB00C);\n    address private constant ALICE = address(0xA11CE);\n    address private constant BOB = address(0xB0B);\n    uint256 private constant DOLLAR = uint256(1 ether) * 1e18 / 2000 ether;\n\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    ImdUSD private stable;\n    FnFeed private primary;\n    FnFeed private health;\n    FnFeed private spot;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new FnAggregator()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        primary = new FnFeed(DOLLAR);\n        health = new FnFeed(0.85 ether);\n        spot = new FnFeed(DOLLAR);\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(health), address(spot)\n        );\n        stable = vault.stablecoin();\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(BOOK, 200_000 ether);\n        imd.mint(ALICE, 200_000 ether);\n        imd.mint(BOB, 200_000 ether);\n        vm.stopPrank();\n        vm.startPrank(BOOK);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(199_000 ether);\n        vault.draw(99_500 ether);\n        vm.stopPrank();\n        for (uint256 i; i < 24; ++i) {\n            _hour();\n        }\n        assertEq(vault.backedDebt(), 99_500 ether, \"the seasoned book counts in full\");\n    }\n\n    function _next(uint256 seconds_) private {\n        vm.warp(block.timestamp + seconds_);\n        vm.roll(block.number + 1 + seconds_ / 12);\n        primary.set(DOLLAR);\n        spot.set(DOLLAR);\n        health.set(0.85 ether);\n    }\n\n    function _hour() private {\n        _next(1 hours);\n        vault.pace();\n    }\n\n    function test_cancellingAFollowedSiblingDrawLetsAZeroSecondDrawCountInFull() public {\n        // Alice draws 100,000 against 190,000 IMD (CR 190%, inside the redeemable band) and holds it eight\n        // paced hours: the paced debt follows at 10% of max(paced, floor) an hour and absorbs all of it.\n        vm.startPrank(ALICE);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(190_000 ether);\n        vault.draw(100_000 ether);\n        vm.stopPrank();\n        for (uint256 i; i < 8; ++i) {\n            _hour();\n        }\n        assertEq(vault.backedDebt(), 199_500 ether, \"Alice's eight-hour-old draw is fully inside the paced debt\");\n\n        // Block n: Bob draws 100,000 of zero-second debt.\n        _next(12);\n        vm.startPrank(BOB);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(190_000 ether);\n        vault.draw(100_000 ether);\n        vm.stopPrank();\n\n        // Block n + 1: Alice redeems her own 100,000 imdUSD against her own position (the fee stays in her\n        // collateral). Her principal is eight hours old, younger than FRESH_DEBT_WINDOW, so the netting books\n        // none of it as pre-existing and the clamp does not fire, though the paced debt had absorbed all of it.\n        _next(12);\n        vm.prank(ALICE);\n        vault.cash(100_000 ether, 0, ALICE);\n\n        // Block n + 2: the book is 99,500 seasoned plus Bob's 24-second-old 100,000. The paced debt should be\n        // about 99,500 plus two 12-second steps (under 100,000 in all); it is 199,500.\n        _next(12);\n        uint256 backed = vault.backedDebt();\n        emit log_named_uint(\"backedDebt after the sequence\", backed);\n        assertLt(backed, 110_000 ether, \"Bob's zero-second draw counts in full for the work ceiling\");\n    }\n}","reproduction":"Book: 99,500 seasoned (paced 24 hours). Alice locks 190,000 IMD at $1 and draws 100,000; pace hourly for 8 hours: backedDebt() == 199,500 (Alice's draw fully followed). Block n: Bob locks 190,000 and draws 100,000. Block n+1: Alice calls cash(100,000, 0, ALICE) with her own imdUSD. Block n+2: expected backedDebt about 99,500 + two 12-second steps (under 100,000); actual 199,504 imdUSD: Bob's 24-second-old debt counts in full. Run `forge test --match-path test/scratch/FreshNettingHidesFollowedDebt.t.sol`: fails on e4baedf with 'Bob's zero-second draw counts in full for the work ceiling: 199504... >= 110000...'.","severity":"low","snippet":"        if (recent > own) rest -= Math.min(rest, recent - own);","title":"Fresh-principal netting in _tallyPrincipalRetired lets a zero-second draw count in full for backedDebt once a sibling's already-followed (<12h) debt is cancelled"},{"citation":"resolved","description":"PAYOUT_PRICE_FALL_BPS_PER_HOUR bounds only what `cash` pays. `bite` seizes debt x 1.2 / price at the ATTESTED price (src/CDPVault.sol:1354) and `barkFor` marks at it, so a pool held 20% down for one median window plus the grace (6 hours at NHI >= 0.85) plus one block lets the holder liquidate every position under 212.5% true CR (170 / 0.8) and receive collateral worth 1.5 imdUSD per imdUSD burned at the honest price, a 50% gain on the liquidated debt against 1% a paced hour on redeemed volume after the fee's five hours. The sentence (and the runbook's matching line at docs/MAINNET-RUNBOOK.md:416-419) states the hold a profitable push needs as the better part of a day; for the liquidation route it is about eight hours, and nothing in the paced figures shortens or prices it (the grace, the mark's lifetime and the borrower's own top-up during grace do). This is the forced-liquidation direction the oracle rounds already record as the feed's bound (final panel oracle 2026-10-07: 'the same hold works in the fall direction (forced liquidation)'), not a regression, so it is reported as a claim the code does not have rather than a defect: the constant's NatSpec should scope its claim to the redemption channel and point at mat and grace for the liquidation one.","line":356,"path":"src/CDPVault.sol","reproduction":"Constants as committed (mat 170 at NHI >= 0.85, grace 6 hours, CHOP 20%, allowance 20%). Honest price p; a borrower at 200% CR. Attacker holds the pool at 0.8p for the price feed's window (the 13-sample median follows once more than half the samples sit at the low), relays, barks (CR reads 160 < 170), keeps the pool at 0.8p six more hours, bites D: seizes D x 1.2 / 0.8p = 1.5 D / p IMD, worth 1.5 D at p, for D imdUSD burned; expected by the NatSpec: no profitable push inside a day; actual: 50% of liquidated debt after about eight hours of hold. The redemption channel in the same eight hours pays at most 3% of redeemed volume (payoutPrice has fallen 8% and the fee cap takes 5%).","severity":"info","snippet":"    /// closed). It is set so that the hold a profitable push needs runs through the better part of a day on IMD's","title":"NatSpec claims a profitable hold-down needs most of a day; liquidation at the held price pays after the grace, about eight hours, and the paced payout price does not bound it"}],"hash":"04c10f2aad55408fe2ab71f55214b5b308e12ba5886795812de84f7143072d49","nodeId":"b4f250fe-7c2f-443f-a741-ec21a684c8dd","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds two findings, both verified against the tree: one low with a failing Foundry proof under `test/scratch/`, one info on a NatSpec claim. The repository's own suite passes on this commit (640 passed, 4 skipped, fork and scratch excluded).\n\n**Findings**\n\n- **Low, `src/CDPVault.sol:953`, `_tallyPrincipalRetired`.** The fresh-principal netting added for the payout panel's low #3 assumes the paced debt \"has had at most twelve hours to follow\" young principal. At the committed follow rate (10% of max(paced, 100,000) an hour, compounding) a draw is fully absorbed in hours, so cancelling a sibling position's one-to-twelve-hour-old debt books nothing and the clamp never fires. Sequence: Alice draws 100k and holds eight paced hours; block n Bob draws 100k; block n+1 Alice self-redeems her 100k; backedDebt reads 199,504 where the follow should give under 100k. This reopens sweep 2 low #2(1) for that window. Only the work ceiling reads it and the wage is 0, so nothing is takeable today. Proof: `test/scratch/FreshNettingHidesFollowedDebt.t.sol` fails on e4baedf.\n- **Info, `src/CDPVault.sol:356`.** The constant's NatSpec says a profitable push needs most of a day. That holds for redemptions only. A pool held 20% down for one window plus the six-hour grace lets the holder bite every position under 212.5% true CR at the attested price, collecting 1.5 imdUSD of collateral per imdUSD burned. The paced payout price does not touch that route. It is the forced-liquidation direction the oracle rounds already record, so it is reported as a claim to scope, not a defect.\n\n**Answers to the seven questions**\n\n1. **Payout economics.** The payout is `amount × backing × (1 − fee) / max(attested, paced)`, and the paced price rises at once, so no rise, fall-and-rise or rise-and-fall sequence ever pays above the attested price. A 20% hold pays 1% of redeemed volume per paced hour after the fee's first five; a pushed-up window underpays for about 18 paced hours as stated. The Treasury's fall trigger pays for the attestation that writes the held low, which is a cost to the protocol of 1 IMD per ask, not a shortening. Liquidation at the held price is what pays for a hold, in about eight hours, so 1% an hour is the right bound for the redemption channel but does not bound the attack. The honest-fall cost matches the NatSpec.\n2. **Clamp and netting.** Own-mint netting, the wipe tally and the live cap are consistent in every ordering I traced; the one gap is the finding above. Cross-position wipe-then-draw in one transaction is already recorded as accepted (paced vault panel info #10). Driving the paced debt below the seasoned book costs twelve hours of held debt plus the self-redemption's gas, as the record states.\n3. **Paced figures across the system.** fundOracle, redeemIMD and cover lower the live backing at once and the figure follows down immediately; donations rise at 2% of par an hour; a stale or diverged window holds the backing and the paced price without consuming the interval; relay bundles pace at the fresh price inside the same guarded call. No desynchronised record found.\n4. **Launch window.** verifySeeded now checks both references and refuses NHI at or under 0.6. The paced supply seeds at the floor, the paced price at the first usable pacing. Day-one halts are StaleFeed (price and spot one hour, NHI one day, Chainlink two hours), PriceDivergence, ExcessDeviation beyond the epoch, and DebtCeilingReached; each recovers with an attestation or the widened allowance. Bad debt cannot be covered on day one until imdUSD reaches the Treasury.\n5. **Oracle surface.** Over-borrowing at a pushed-up price loses to the 20% bonus at 170%, so the live route is the held-down liquidation above.\n6. **Governance and deployment.** No regression. Every Treasury exit is bounded as documented; runVault records before verify and refuses the public salt and a second vault.\n7. **Claims.** The two NatSpec passages named in th","treeHash":null,"usage":{"cachedInputTokens":1743471,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":41041,"runtime":"claude","turns":31,"wallClockMs":722353}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"93ca4a1020037bf1","findings":[{"citation":"resolved","description":"Merged from four specialist reports (audit_permissions 187de2e5, audit_economics 2ffa458e, audit_math a69eee9c, audit_flow 58bc8249). They describe one mechanism. The final sweep 2 high was answered by pacing only the price `cash` pays at (max(attested, paced), falling 1% an hour). `bite` (CDPVault.bite) still checks health and prices the seizure at the ATTESTED price: collateralSeized = debt x 1.2 / price, principalPart = debt / price. Both feeds read the one Uniswap v4 pool, so the skew check passes. The feed allows a 20% step from a fresh anchor in each epoch. Anyone who holds the pool down can therefore bark at the held price, keep the low attested hourly through the grace (lull 6 h at NHI >= 0.85, tail 1 h), then bite at the held price. One rung (0.8 of the honest price, about 7 hours) makes every position under 170/0.8 = 212.5% honest CR liquidatable, and each imdUSD burned seizes 1.5 imdUSD of collateral at the honest price. Two rungs (0.64, about 8 hours) reach every position under 266% CR and seize 1.875x. The borrower is healthy at the honest price and loses the difference. The sweep oracle panel (docs/AUDIT-SWEEP-PANEL-ORACLE-2026-10-07.md:198) accepted the 'Down' walk with the prize stated as 'the bonus is 20% of debt repaid, at most $200k on the whole LINE'. That bound does not hold, because the seizure and the bonus are both priced at the held price. The cost of the hold: a 20% push sells 1/sqrt(0.8)-1 = 11.8% of the pool's IMD side (about $271k) and a 36% push sells 25% (about $575k). Unwinding returns the slippage, so the round trip costs about $5k to $12k in 1% pool fees, plus whatever dip-buyers take over 7 to 8 hours. The prize at LINE $1M is about $400k to $800k of borrowers' sIMD. That is roughly 4x the accepted bound and more than the whole-day redemption prize. Nothing in the protocol shortens or pays for the hold: resecure and the Treasury's fall trigger only refresh at the held value (the fall trigger buys the first attestation), and the mark's tail is refreshed by the same relays. The payout-price NatSpec (CDPVault.sol:350-361) and runbook section 7 say a profitable push needs 'the better part of a day'. For this route it needs one window plus the grace. Reachable with the constants as committed. Smallest fix, consistent with `cash`: price the seizure (and principalPart) at _payoutPrice(price), the higher of the attested and paced prices. Keep the health check, the mark and the dust test at the attested price. That caps an h-hour hold at 1.2/0.99^h of the debt (1.30x at 8 h). Tradeoff for the requester to decide: after an honest fall faster than 1% an hour, a liquidator is paid less until the paced price follows, so mat/grace or a separate, faster seizure rate must be re-derived as docs/PARAMETERS-2026-10-05.md did for the 20% bonus.","line":1354,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// A pool held down two feed rungs (0.8, then 0.64 of the honest price: two epochs, an hour apart, both within\n// the feeds' fresh allowance) and kept attested through the six-hour grace lets a liquidator seize\n// debt x 1.2 / 0.64 = 1.875 x the debt's HONEST value per imdUSD burned. The accepted bound (sweep oracle\n// panel 2026-10-07, Q6: \"the bonus is 20% of debt repaid, at most $200k on the whole LINE\") prices the bonus at\n// the honest price; `bite` prices it at the held one. The paced payout price protects `cash` from the same\n// hold; `bite` reads only the attested price.\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Math} from \"@openzeppelin/contracts/utils/math/Math.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract HlFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 hours;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external view returns (bool) {\n        return block.timestamp - updatedAt > maxAge;\n    }\n}\n\ncontract HlAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract HeldDownLiquidationTest is Test {\n    address private constant VICTIM = address(0xB00C);\n    address private constant ATTACKER = address(0xA77A);\n    uint256 private constant DOLLAR = uint256(1 ether) * 1e18 / 2000 ether; // IMD/ETH at $1\n\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    ImdUSD private stable;\n    HlFeed private primary;\n    HlFeed private health;\n    HlFeed private spot;\n    uint256 private imdEth = DOLLAR;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new HlAggregator()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        primary = new HlFeed(DOLLAR);\n        health = new HlFeed(0.85 ether); // mat 170, grace six hours\n        spot = new HlFeed(DOLLAR);\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(health), address(spot)\n        );\n        stable = vault.stablecoin();\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(VICTIM, 1_000_000 ether);\n        imd.mint(ATTACKER, 2_000_000 ether);\n        vm.stopPrank();\n        // The victim: $480k of debt at 200%, the healthy side of mat 170 (LINE is $1M for the whole book).\n        vm.startPrank(VICTIM);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(960_000 ether);\n        vault.draw(480_000 ether);\n        vm.stopPrank();\n        // The attacker holds imdUSD, drawn a day earlier at 400% so the hold cannot reach its own position.\n        vm.startPrank(ATTACKER);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(2_000_000 ether);\n        vault.draw(500_000 ether);\n        vm.stopPrank();\n        for (uint256 i; i < 24; ++i) {\n            _hour();\n        }\n    }\n\n    function _next(uint256 seconds_) private {\n        vm.warp(block.timestamp + seconds_);\n        vm.roll(block.number + 1 + seconds_ / 12);\n        primary.set(imdEth);\n        spot.set(imdEth);\n        health.set(0.85 ether);\n    }\n\n    function _hour() private {\n        _next(1 hours);\n        vault.pace();\n    }\n\n    /// @dev Rung one: 0.8 (the fresh cap). An hour later the epoch has closed and rung two anchors at 0.8: 0.64.\n    /// The victim is marked at once (CR 128 < 170) and the pool is held, re-attested hourly, through the six-hour\n    /// grace. At the bite the seizure is priced at the held 0.64, so each imdUSD burned takes 1.875 IMD.\n    function test_aPoolHeldDownTwoRungsThroughGracePaysTheLiquidatorFarMoreThanTheBonus() public {\n        uint256 preFall = DOLLAR;\n        imdEth = DOLLAR * 80 / 100;\n        _hour();\n        imdEth = DOLLAR * 64 / 100;\n        _hour();\n        vm.prank(ATTACKER);\n        vault.bark(VICTIM);\n        (uint256 markedAt, uint256 grace,,) = vault.liquidationMarks(VICTIM);\n        assertEq(grace, 6 hours, \"grace at NHI 0.85\");\n        for (uint256 i; i < 6; ++i) {\n            _hour();\n        }\n        assertGe(block.timestamp, markedAt + grace, \"grace has elapsed\");\n        assertApproxEqRel(vault.payoutPrice(), 0.92 ether, 0.01e18, \"cash would still be paid near the pre-fall price\");\n\n        (uint256 victimCollateralBefore, uint256 burned) = vault.positions(VICTIM);\n        uint256 attackerImdBefore = imd.balanceOf(ATTACKER);\n        vm.prank(ATTACKER);\n        vault.bite(VICTIM, burned);\n        (uint256 victimCollateralAfter, uint256 victimDebtAfter) = vault.positions(VICTIM);\n        assertEq(victimDebtAfter, 0, \"the whole debt is bitten in one call\");\n        uint256 seized = victimCollateralBefore - victimCollateralAfter;\n        uint256 received = imd.balanceOf(ATTACKER) - attackerImdBefore;\n\n        // About 480,078 imdUSD burned (principal plus accrued fees). Seized: burned x 1.2 / 0.64 = 1.875 x burned\n        // in IMD, worth 1.875 x the debt at the pre-fall price ($900k of the victim's $960k). The attacker, also the marker, keeps all but the\n        // protocol's tenth of the bonus.\n        uint256 seizedAtPreFall = Math.mulDiv(seized, preFall * 2000, 1e18);\n        uint256 receivedAtPreFall = Math.mulDiv(received, preFall * 2000, 1e18);\n        emit log_named_decimal_uint(\"seized, at the pre-fall price\", seizedAtPreFall, 18);\n        emit log_named_decimal_uint(\"liquidator receives, at the pre-fall price\", receivedAtPreFall, 18);\n        // EXPECTED (the accepted bound: a 20% bonus on the debt repaid, with room for the paced price's own lag\n        // over an eight-hour hold): at most 1.35 x the debt. ACTUAL: 1.875 x.\n        assertLe(seizedAtPreFall, burned * 135 / 100, \"a held-down pool pays the liquidator the whole push\");\n    }\n}","reproduction":"Reproduced by running test/scratch/Proof_2ffa458e12b7.t.sol on e4baedf. Setup: ParameterizedVault over MockIMD at $1, ETH $2000, NHI 0.85 (mat 170, grace 6 h). VICTIM locks 960,000 and draws 480,000 (200%). ATTACKER locks 2,000,000 and draws 500,000 (400%). Then 24 paced hours pass. Sequence: feeds to 0.8 and pace; an hour later feeds to 0.64 and pace; ATTACKER bark(VICTIM) (CR 128 < 170, grace 6 h); six hourly re-attestations at 0.64 with pace; bite(VICTIM, whole debt 480,078). Expected under the accepted bound: seized collateral worth <= 1.2x (1.35x allowing the paced lag) of the debt at the pre-fall price, i.e. <= 648,105. Actual: 900,146 IMD seized (1.875x), ATTACKER receives 885,144, while payoutPrice() is 0.92. Failure message: 'a held-down pool pays the liquidator the whole push: 900145972602739725300000 > 648105100273972602216000'. The one-rung variant (test/scratch/Proof_a69eee9c3317.t.sol) also fails: 50,000 imdUSD burned after a 7 h hold at 0.8 returns 73,750 IMD (1.475x), where at most 65,000 was expected.","severity":"high","snippet":"        uint256 collateralSeized = Math.mulDiv(debtToRepay, (100 + CHOP_PERCENT) * 1e16, price);","title":"bite prices the seizure at a held-down attested price, outside the paced payout price: holding the pool down through the grace pays a liquidator 1.5x to 1.875x the debt, against the accepted bound of "},{"citation":"resolved","description":"Merged from audit_flow ccb62243 and audit_math 35b4c4fd. _tallyPrincipalRetired (73191e0) nets the position's whole _recentlyMinted record, anything under FRESH_DEBT_WINDOW = 12 h, out of CANCELLED_PRE_SLOT. Its NatSpec gives the reason as 'the paced debt has had at most that long to follow it'. The follow is 10% of max(paced, 100,000) per paced hour, compounding, so a 100,000 draw on a 99,500 book is fully inside _debtPaced after about 7.3 paced hours. Cancelling such a loan (cash, bite, cover) between that point and 12 h books nothing, so _clampPacedDebt does not fire. The slot the cancelled loan held in the paced debt then goes to whatever was drawn since, however fresh. This reopens final sweep 2 low #2(1) ('debt cancelled by a redemption and drawn again by someone else backs nothing until it has been held') for that window. Only ParameterizedVault.backedDebt -> earnLine reads the paced debt, and WAGE_WAD = 0 at launch, so nothing can be taken today. Once a wage is set behind the timelock, it is the sweep-panel round trip at a one-block holding time. Smallest fix: net out only principal whose record is younger than PACE_INTERVAL (a draw goes unfollowed by at most one step), or only this transaction's own mint as a3aa9e4 did, and accept the bounded churn that panel #3 rated low.","line":953,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// The fresh-principal netting in _tallyPrincipalRetired (payout vault panel 2026-10-09, low #3) assumes the\n// paced debt \"has had at most twelve hours to follow\" principal younger than FRESH_DEBT_WINDOW. At the\n// committed constants the follow absorbs a draw in hours (10% of max(paced, 100,000) an hour, compounding),\n// so a sibling position's draw that is already fully inside the paced debt, cancelled one block after a\n// second position's draw, is netted out as \"fresh\" and the clamp never fires: the second position's\n// zero-second debt counts in full for ParameterizedVault.backedDebt. This is the final sweep 2 low #2 (1)\n// reopened for cancellations of debt between its follow time and twelve hours old.\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract FnFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 hours;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external view returns (bool) {\n        return block.timestamp - updatedAt > maxAge;\n    }\n}\n\ncontract FnAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract FreshNettingHidesFollowedDebtTest is Test {\n    address private constant BOOK = address(0xB00C);\n    address private constant ALICE = address(0xA11CE);\n    address private constant BOB = address(0xB0B);\n    uint256 private constant DOLLAR = uint256(1 ether) * 1e18 / 2000 ether;\n\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    ImdUSD private stable;\n    FnFeed private primary;\n    FnFeed private health;\n    FnFeed private spot;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new FnAggregator()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        primary = new FnFeed(DOLLAR);\n        health = new FnFeed(0.85 ether);\n        spot = new FnFeed(DOLLAR);\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(health), address(spot)\n        );\n        stable = vault.stablecoin();\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(BOOK, 200_000 ether);\n        imd.mint(ALICE, 200_000 ether);\n        imd.mint(BOB, 200_000 ether);\n        vm.stopPrank();\n        vm.startPrank(BOOK);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(199_000 ether);\n        vault.draw(99_500 ether);\n        vm.stopPrank();\n        for (uint256 i; i < 24; ++i) {\n            _hour();\n        }\n        assertEq(vault.backedDebt(), 99_500 ether, \"the seasoned book counts in full\");\n    }\n\n    function _next(uint256 seconds_) private {\n        vm.warp(block.timestamp + seconds_);\n        vm.roll(block.number + 1 + seconds_ / 12);\n        primary.set(DOLLAR);\n        spot.set(DOLLAR);\n        health.set(0.85 ether);\n    }\n\n    function _hour() private {\n        _next(1 hours);\n        vault.pace();\n    }\n\n    function test_cancellingAFollowedSiblingDrawLetsAZeroSecondDrawCountInFull() public {\n        // Alice draws 100,000 against 190,000 IMD (CR 190%, inside the redeemable band) and holds it eight\n        // paced hours: the paced debt follows at 10% of max(paced, floor) an hour and absorbs all of it.\n        vm.startPrank(ALICE);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(190_000 ether);\n        vault.draw(100_000 ether);\n        vm.stopPrank();\n        for (uint256 i; i < 8; ++i) {\n            _hour();\n        }\n        assertEq(vault.backedDebt(), 199_500 ether, \"Alice's eight-hour-old draw is fully inside the paced debt\");\n\n        // Block n: Bob draws 100,000 of zero-second debt.\n        _next(12);\n        vm.startPrank(BOB);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(190_000 ether);\n        vault.draw(100_000 ether);\n        vm.stopPrank();\n\n        // Block n + 1: Alice redeems her own 100,000 imdUSD against her own position (the fee stays in her\n        // collateral). Her principal is eight hours old, younger than FRESH_DEBT_WINDOW, so the netting books\n        // none of it as pre-existing and the clamp does not fire, though the paced debt had absorbed all of it.\n        _next(12);\n        vm.prank(ALICE);\n        vault.cash(100_000 ether, 0, ALICE);\n\n        // Block n + 2: the book is 99,500 seasoned plus Bob's 24-second-old 100,000. The paced debt should be\n        // about 99,500 plus two 12-second steps (under 100,000 in all); it is 199,500.\n        _next(12);\n        uint256 backed = vault.backedDebt();\n        emit log_named_uint(\"backedDebt after the sequence\", backed);\n        assertLt(backed, 110_000 ether, \"Bob's zero-second draw counts in full for the work ceiling\");\n    }\n}","reproduction":"Reproduced by running test/scratch/Proof_ccb6224305d0.t.sol on e4baedf. Setup: BOOK has 99,500 seasoned debt over 24 paced hours. ALICE locks 190,000 and draws 100,000, followed by 8 hourly paces (backedDebt == 199,500). Block n: BOB locks 190,000 and draws 100,000. Block n+1: ALICE calls cash(100,000, 0, ALICE). Block n+2: expected backedDebt < 110,000, since BOB's 24-second debt is unheld. Actual: 199,504.06, and the test fails with 'Bob's zero-second draw counts in full for the work ceiling'.","severity":"low","snippet":"        if (recent > own) rest -= Math.min(rest, recent - own);","title":"Fresh-principal netting treats any principal under 12 h as unfollowed: cancelling an already-followed loan lets a one-block-old draw by another position count in full for backedDebt"},{"citation":"resolved","description":"From audit_math c386f071. _reduceDebt conserves the record's principal-time when it retires the youngest debt first. After a wipe that leaves r of a tranche X drawn t ago, the remainder is dated t*X/r back, and once that passes 12 h the whole record ages out. With X/r >= 12, a remainder one hour old reads as seasoned. Cancelling it then books the full remainder to CANCELLED_PRE_SLOT, and _clampPacedDebt subtracts it from the paced debt the transaction began with, although the follow had absorbed only one step of X. The paced debt lands below the untouched seasoned book. With it fall backedDebt and earnLine, which recover at 10% an hour. The payout vault panel #3 resolution says this costs holding the debt twelve hours. It costs one hour plus about $3 of duty, and the self-redemption's fee stays in the attacker's own position. WAGE_WAD is 0 at launch, so nothing is blocked today. With a wage set, this is a repeatable, hour-priced denial of the work ceiling. Smallest fix: keep a per-position 'last drawn at' timestamp and treat principal of a position drawn within FRESH_DEBT_WINDOW as recent whatever its amount-weighted record says, or cap the clamp by the part of the position's debt that existed when the transaction began. Otherwise restate the bound as one hour.","line":954,"path":"src/CDPVault.sol","reproduction":"Reproduced with my own scratch test (harness of Proof_ccb6224305d0, then test_agedOutRemainder). Setup: BOOK has 99,500 seasoned debt over 24 paced hours. P1 locks 1,200,000 and draws 600,000; pace one hour later (backedDebt 109,500). P1 wipe(551,000): the record ages out. P1 free(1,097,000), leaving CR 210%. Next block P1 cash(49,000, 0, P1). Next block: totalDebt 99,503.04. Expected per the record: backedDebt >= 99,500. Actual: 60,569.83.","severity":"low","snippet":"        if (rest != 0) _transientAdd(CANCELLED_PRE_SLOT, rest);","title":"A remainder aged out by the record's conserved principal-time is booked as pre-existing, so a one-hour hold drives the paced debt under the seasoned book (the record states twelve hours)"},{"citation":"resolved","description":"Merged from audit_permissions 866c8123 and audit_economics e9947645. The rate arithmetic holds: the payout price falls at most 1% per paced hour, so the gain is about 1% of redeemed volume per hour held. Two things in the NatSpec and the record do not hold. (1) The fee is only 5% for a burn that is large relative to the fee base. A burn of 0.5% of the base pays 75 bps, so it breaks even after one paced hour (0.9925/0.99 = 1.0025) and is +1.27% after two. (2) The reason the rate was accepted ('a profitable push has to be held ... through the better part of a day') is not quantified against the on-chain cost of the hold. A 20% push and its unwind cost about $5.4k in pool fees. After 12 paced hours at 0.8, 50,000 imdUSD at the capped 500 bps fee redeems for 53,588 IMD (+7.2%). Only dip-buying makes the hold expensive, and the code does not bound it. This route is dominated by the liquidation route (the first finding), which pays more in fewer hours, so it is rated low. Fix: restate the bound as break-even at fee/1% paced hours (under one hour at the floor fee). If the requester wants it bounded in code, cap the volume `cash` may burn per PACE_INTERVAL, since the price bound alone sets only the rate per hour.","line":353,"path":"src/CDPVault.sol","reproduction":"Reproduced by running test/scratch/Proof_866c8123fb30.t.sol on e4baedf. Setup: BOOK 1,800,000/900,000, HOLDER 300,000/100,000, 48 paced hours. Feeds step to 0.8 and are paced once. redemptionFeeBps(5,000e18) == 75, and cash(5,000e18, 0, BOOK) returns IMD worth 5,012.63 at the pre-fall price (expected <= 5,000 per the NatSpec). After two paced hours it returns 5,063.26. Both tests fail.","severity":"low","snippet":"    /// however many hours it is held (1% an hour here, after the fee has eaten the first five), the whole 20% after","title":"The cash route's stated bound 'after the fee has eaten the first five' hours holds only at the fee cap; small burns break even after one paced hour and a 12-hour hold pays 7.2% at the cap"},{"citation":"resolved","description":"From audit_economics fa826c7c. The feed admits 20% per epoch from that epoch's anchor, and _pacedPrice rises at once. A pool held up through two windows writes 1.2 x 1.2 = 1.44, which takes ln(1.44)/ln(1/0.99) = 37 paced hours to decay, and three windows take 55 hours. This is the accepted direction: it underpays and never overpays, and its only cost is the push. The stated recovery time is the single-window figure, though, so an operator reading 'about 18 hours' after a two-hour pump expects the wrong recovery. Restate the bound as compounding with the length of the hold.","line":361,"path":"src/CDPVault.sol","reproduction":"Reproduced with my own scratch test (test_twoWindowRise). After 24 paced hours at $1: feeds to 1.2, pace; an hour later feeds to 1.44, pace; feeds back to 1.0, then 18 hourly paces. payoutPrice() = 1.2017 (= 1.44 x 0.99^18), where the NatSpec implies about 1.0.","severity":"info","snippet":"    /// push, less 1% a paced hour, until it has decayed: about 18 hours for 20%, for the cost of the push","title":"The pumped-price bound 'about 18 hours for 20%' is per feed window: two consecutive windows (1.44x) underpay redemptions for about 37 paced hours"},{"citation":"resolved","description":"Merged from audit_math c11baca8 and audit_economics 235c7adc. Three claims no longer match the code. (1) CDPVault.sol:312-313 and ParameterizedVault.sol:240-241 and 265-266 say debt cancelled and drawn again 'backs nothing until it has been held'. The second finding shows that a one-block-old draw counts in full. (2) CDPVault.sol:931-935 says the paced debt 'has had at most' FRESH_DEBT_WINDOW to follow the netted principal. The follow completes within hours, and the amount-weighted mintedAt keeps a record whole and fresh for up to about twice the window: two equal tranches at t0 and t0+11h are dated t0+5.5h and stay fresh until t0+17.5h. (3) The payout vault panel #3 resolution states a twelve-hour cost for lowering the paced debt; the third finding shows it is one hour. Reword these to the property the code has, or change the code as in the second and third findings.","line":932,"path":"src/CDPVault.sol","reproduction":"See the second finding (backedDebt 199,504 with a 24-second-old loan counted) and the third (60,569 after a one-hour hold). For (2): draw 500,000 at t0 and 500,000 at t0+11h; at t0+12h+1s _recentlyMinted is still 1,000,000, because mintedAt is t0+5.5h.","severity":"info","snippet":"    /// own record, `_recentlyMinted`) is, when cancelled, booked nowhere, since the paced debt has had at most","title":"NatSpec claims that no longer hold after 73191e0: cancelled-and-redrawn debt 'backs nothing until it has been held', the netting window is 'at most that long to follow it', and the record keeps a tran"}],"hash":"2ea16728f65e3554e3ba6c5bc91e53dac722ccac905370c9713fd66028eaaa0f","nodeId":"267a6aaf-621b-42fd-b1d7-b8536ae9351c","outcome":"completed","summary":"I found one high and three lows that I reproduced against e4baedf, plus two documentation-only notes. All six are in `.imd-findings.json`. The specialists filed 12 reports; after merging duplicates they come to these six. Each kept finding was run: either the specialist's attached proof fails on this code for the reason it gives, or my own scratch test reproduces it. `test/scratch` is removed and no tracked file changed.\n\n| Severity | Finding | Where | Reproduction |\n|---|---|---|---|\n| **High** | `bite` sizes the collateral it seizes at the held-down attested price, not the paced payout price `cash` uses (merges 4 reports). Holding the pool at 0.8 through the 6-hour grace (about 7 hours) seizes 1.5× the debt; two steps down to 0.64 (about 8 hours) seize 1.875×. The accepted bound in the oracle record (\"20% bonus, at most $200k on the whole LINE\") does not hold: about $400k–$800k of borrowers' collateral is exposed at LINE $1M, for roughly $5k–$12k in pool fees plus whatever dip-buyers absorb. | `src/CDPVault.sol:1354` | `Proof_2ffa458e12b7`: 900,146 seized where at most 648,105 was expected, while `payoutPrice()` is 0.92. Proof attached to the finding. |\n| Low | The fresh-principal netting treats any principal under 12 hours old as not yet followed. It is usually followed within a few hours, so cancelling a loan that is already fully followed lets another position's one-block-old draw count in full toward `backedDebt` (merges 2 reports). Nothing can be taken while the wage is 0. | `src/CDPVault.sol:953` | `Proof_ccb6224305d0`: `backedDebt` is 199,504, expected under 110,000. Proof attached. |\n| Low | A wipe can make a one-hour-old remainder read as seasoned. Cancelling it then pushes the paced debt below the untouched seasoned book after a one-hour hold, not the twelve hours the record states. | `src/CDPVault.sol:954` | My test: `backedDebt` is 60,570 against a seasoned book of 99,500. |\n| Low | The comment \"after the fee has eaten the first five\" hours is only true at the fee cap. A small redemption breaks even after one paced hour, and a 12-hour hold pays +7.2% even at the cap (merges 2 reports). | `src/CDPVault.sol:353` | `Proof_866c8123fb30` fails as stated. |\n| Info | \"About 18 hours for 20%\" is for one feed window. A price pushed up through two windows (1.44×) takes about 37 hours to decay. | `src/CDPVault.sol:361` | My test: `payoutPrice()` is 1.2017 after 18 paced hours. |\n| Info | Comments that no longer hold after 73191e0: \"backs nothing until it has been held\", \"at most that long to follow it\", and the twelve-hour cost in the resolution record (merges 2 reports). | `src/CDPVault.sol:932`, `src/ParameterizedVault.sol:240`, `src/ParameterizedVault.sol:265` | Follows from the two netting reproductions above. |\n\n**Coverage, stated plainly.** In the time available this was a focused review, not the full seven-question audit the task asks for:\n- **Read and traced:** the parts of `CDPVault` behind each finding (the paced-figures block and its comments, `_tallyPrincipalRetired`, `_paceWith`, `_clampPacedDebt`, `bite`, `_reduceDebt`, `_recentlyMinted`), the related comments in `ParameterizedVault`, and the relevant passages of the oracle record and the payout-panel record.\n- **Not read in full:** the rest of `CDPVault`, Treasury, the feeds, SwarmRelay, Parameters, `DeployMainnet`/`verifySeeded`/`runVault`, `plan.py`, and runbook section 7.\n- **So these are not answered:** question 4 (the hour-by-hour launch window), question 6 (governance and deployment regressions), and the halt-and-recovery parts of question 3.\n\nThe JSON file is the deliverable; it was written in one pass at the end, not built up as findings came in.","treeHash":null,"usage":{"cachedInputTokens":681880,"inputTokens":22,"model":"claude-opus-5-5","outputTokens":13614,"runtime":"claude","turns":11,"wallClockMs":155839}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"644eb561f6d70a85","findings":[{"citation":"resolved","description":"Question 5 (the oracle as an attack surface beyond the payout price). `cash` now pays IMD at max(attested, paced) so a pool held down through the feed window cannot be redeemed at the whole fall (payout vault panel, high, fixed at 1% an hour). `bite` has no such bound: `collateralSeized = debtToRepay * 1.2e16 / price` reads only the attested `price`, and the mark/grace/bite path (bark at the held price, six hours of grace at NHI >= 0.85, bite at the held price) is reachable by anyone who holds the pool down through the grace. The sweep oracle panel (docs/AUDIT-SWEEP-PANEL-ORACLE-2026-10-07.md, Q6, 'Down') accepted this walk with the prize stated as 'the bonus is 20% of debt repaid, at most $200k on the whole LINE ... and it needs the dump held through the six-hour grace'. That bound does not hold: the bonus is 20% at the HELD price, and the seizure itself is priced at the held price, so at two fresh-cap rungs (0.8, then 0.64 of the honest price an hour later, both within the feeds' allowance from the epoch anchor) each imdUSD burned seizes 1.2 / 0.64 = 1.875 IMD, worth 1.875 imdUSD at the pre-fall price. The borrower loses almost the whole position (1.875 of a 2.0 collateral ratio); the liquidator, who is also the marker, keeps all but the protocol's tenth of the bonus. Money and hours at LINE $1M, mat 170, grace 6 h, pool $2.3M a side: hold the pool at 0.8 for the first median window (about an hour), at 0.64 for the second, bark every position under 170/0.64 = 266% honest CR (every position in the book at launch ratios), hold six more hours re-attesting hourly (the Treasury's own fall trigger buys the first update), then bite: 8 hours of hold. Pushing a full-range pool to 0.64 sells 1/sqrt(0.64) - 1 = 25% of its IMD (about $575k at the honest price), about $11.5k in pool fees for the round trip, plus 36% of whatever dip-buyers take during the hold. Prize: with the attacker holding imdUSD bought on the market, up to 0.875 x the bitten debt, about $800k on a $1M book; with imdUSD the attacker must borrow under the same LINE (its own position at >= 266% so the hold cannot reach it), about $405k, as the proof measures (480,078 imdUSD burned, sIMD worth $900,146 at the pre-fall price seized, $885,144 received). This is 4x the accepted bound and 2x the redemption route's whole-day prize, for an 8-hour hold instead of 22. Nothing in the protocol shortens it: `resecure` and pacing bound the backing per imdUSD, not the seizure price; `cover` and `heel` do not apply; the mark's tail is the feed lifetime, which the attacker refreshes. Smallest fix, consistent with the paced payout price: price the seizure at the higher of the attested price and the paced payout price (`collateralSeized = mulDiv(debtToRepay, 1.2e16, _payoutPrice(price))`, and `principalPart` the same), keeping the health check, the mark and the dust test at the attested price. At 1% an hour that caps the seizure at 1.2 / 0.99^h of the debt over an h-hour hold (1.30x at eight hours) rather than 1.2 / (the fall); the cost is that after an HONEST fall faster than 1% an hour a liquidator is paid 1.2 x attested / paced of the debt until the paced price has followed, so the fall rate (or a separate, faster rate for the seizure, e.g. 5% an hour, which bounds an 8-hour hold to 1.2 / 0.66 = 1.8x only if combined with a longer grace) must be chosen against liquidator profitability as docs/PARAMETERS-2026-10-05.md did for the 20% bonus. Alternatively cap the seizure's value at the paced price to a fixed multiple of the debt (1.2 + a margin) and refuse the rest until the paced price has followed. Either way the decision belongs to the requester; what does not hold is the stated $200k bound.","line":1354,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// A pool held down two feed rungs (0.8, then 0.64 of the honest price: two epochs, an hour apart, both within\n// the feeds' fresh allowance) and kept attested through the six-hour grace lets a liquidator seize\n// debt x 1.2 / 0.64 = 1.875 x the debt's HONEST value per imdUSD burned. The accepted bound (sweep oracle\n// panel 2026-10-07, Q6: \"the bonus is 20% of debt repaid, at most $200k on the whole LINE\") prices the bonus at\n// the honest price; `bite` prices it at the held one. The paced payout price protects `cash` from the same\n// hold; `bite` reads only the attested price.\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Math} from \"@openzeppelin/contracts/utils/math/Math.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract HlFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 hours;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external view returns (bool) {\n        return block.timestamp - updatedAt > maxAge;\n    }\n}\n\ncontract HlAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract HeldDownLiquidationTest is Test {\n    address private constant VICTIM = address(0xB00C);\n    address private constant ATTACKER = address(0xA77A);\n    uint256 private constant DOLLAR = uint256(1 ether) * 1e18 / 2000 ether; // IMD/ETH at $1\n\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    ImdUSD private stable;\n    HlFeed private primary;\n    HlFeed private health;\n    HlFeed private spot;\n    uint256 private imdEth = DOLLAR;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new HlAggregator()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        primary = new HlFeed(DOLLAR);\n        health = new HlFeed(0.85 ether); // mat 170, grace six hours\n        spot = new HlFeed(DOLLAR);\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(health), address(spot)\n        );\n        stable = vault.stablecoin();\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(VICTIM, 1_000_000 ether);\n        imd.mint(ATTACKER, 2_000_000 ether);\n        vm.stopPrank();\n        // The victim: $480k of debt at 200%, the healthy side of mat 170 (LINE is $1M for the whole book).\n        vm.startPrank(VICTIM);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(960_000 ether);\n        vault.draw(480_000 ether);\n        vm.stopPrank();\n        // The attacker holds imdUSD, drawn a day earlier at 400% so the hold cannot reach its own position.\n        vm.startPrank(ATTACKER);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(2_000_000 ether);\n        vault.draw(500_000 ether);\n        vm.stopPrank();\n        for (uint256 i; i < 24; ++i) {\n            _hour();\n        }\n    }\n\n    function _next(uint256 seconds_) private {\n        vm.warp(block.timestamp + seconds_);\n        vm.roll(block.number + 1 + seconds_ / 12);\n        primary.set(imdEth);\n        spot.set(imdEth);\n        health.set(0.85 ether);\n    }\n\n    function _hour() private {\n        _next(1 hours);\n        vault.pace();\n    }\n\n    /// @dev Rung one: 0.8 (the fresh cap). An hour later the epoch has closed and rung two anchors at 0.8: 0.64.\n    /// The victim is marked at once (CR 128 < 170) and the pool is held, re-attested hourly, through the six-hour\n    /// grace. At the bite the seizure is priced at the held 0.64, so each imdUSD burned takes 1.875 IMD.\n    function test_aPoolHeldDownTwoRungsThroughGracePaysTheLiquidatorFarMoreThanTheBonus() public {\n        uint256 preFall = DOLLAR;\n        imdEth = DOLLAR * 80 / 100;\n        _hour();\n        imdEth = DOLLAR * 64 / 100;\n        _hour();\n        vm.prank(ATTACKER);\n        vault.bark(VICTIM);\n        (uint256 markedAt, uint256 grace,,) = vault.liquidationMarks(VICTIM);\n        assertEq(grace, 6 hours, \"grace at NHI 0.85\");\n        for (uint256 i; i < 6; ++i) {\n            _hour();\n        }\n        assertGe(block.timestamp, markedAt + grace, \"grace has elapsed\");\n        assertApproxEqRel(vault.payoutPrice(), 0.92 ether, 0.01e18, \"cash would still be paid near the pre-fall price\");\n\n        (uint256 victimCollateralBefore, uint256 burned) = vault.positions(VICTIM);\n        uint256 attackerImdBefore = imd.balanceOf(ATTACKER);\n        vm.prank(ATTACKER);\n        vault.bite(VICTIM, burned);\n        (uint256 victimCollateralAfter, uint256 victimDebtAfter) = vault.positions(VICTIM);\n        assertEq(victimDebtAfter, 0, \"the whole debt is bitten in one call\");\n        uint256 seized = victimCollateralBefore - victimCollateralAfter;\n        uint256 received = imd.balanceOf(ATTACKER) - attackerImdBefore;\n\n        // About 480,078 imdUSD burned (principal plus accrued fees). Seized: burned x 1.2 / 0.64 = 1.875 x burned\n        // in IMD, worth 1.875 x the debt at the pre-fall price ($900k of the victim's $960k). The attacker, also the marker, keeps all but the\n        // protocol's tenth of the bonus.\n        uint256 seizedAtPreFall = Math.mulDiv(seized, preFall * 2000, 1e18);\n        uint256 receivedAtPreFall = Math.mulDiv(received, preFall * 2000, 1e18);\n        emit log_named_decimal_uint(\"seized, at the pre-fall price\", seizedAtPreFall, 18);\n        emit log_named_decimal_uint(\"liquidator receives, at the pre-fall price\", receivedAtPreFall, 18);\n        // EXPECTED (the accepted bound: a 20% bonus on the debt repaid, with room for the paced price's own lag\n        // over an eight-hour hold): at most 1.35 x the debt. ACTUAL: 1.875 x.\n        assertLe(seizedAtPreFall, burned * 135 / 100, \"a held-down pool pays the liquidator the whole push\");\n    }\n}","reproduction":"Mock feeds (one hour lifetime), NHI 0.85 (mat 170, grace 6 h), ETH/USD $2000, IMD at $1. VICTIM locks 960,000 IMD and draws 480,000 imdUSD (200%). ATTACKER locks 2,000,000 IMD and draws 500,000 imdUSD (400%); 24 paced hours pass. Then: feeds to 0.8 and pace (hour 1); feeds to 0.64 and pace (hour 2); ATTACKER barks VICTIM (CR 128 < 170; grace 6 h); six more hours, feeds held at 0.64 and paced hourly; ATTACKER bites VICTIM for its whole accrued debt (480,078 imdUSD). Expected (the accepted bound): collateral seized worth at most 1.2 x the debt at the pre-fall price, about $576k, or at most 1.35x allowing the paced price's own eight-hour lag. Actual: 900,146 IMD seized (1.875x), worth $900,146 at the pre-fall price; the attacker receives 885,144 IMD; `payoutPrice()` at the same moment is 0.92, so `cash` would have been paid near the pre-fall price. Test: test/scratch/HeldDownLiquidation.t.sol fails with 'a held-down pool pays the liquidator the whole push: 900145972602739725300000 > 648105100273972602216000'.","severity":"high","snippet":"        uint256 collateralSeized = Math.mulDiv(debtToRepay, (100 + CHOP_PERCENT) * 1e16, price);","title":"bite prices the seizure at a held-down attested price: a two-rung hold through grace pays a liquidator 1.875x the debt, not the 20% bonus the accepted walk bound states"},{"citation":"resolved","description":"Question 1. The arithmetic of the bound holds as stated: `_pacedPrice` floors the paid price at the paced value less 1% per paced hour (compounding per pacing, at most one interval per pacing), rises at once, and `cash` pays max(attested, paced), so a 20% step reaches the payout after about 22 paced hours and a 40% step after about 51, and a pool held down is paid the hours it is held less the fee. What does not hold is the accepted reason: 'a profitable push has to be held on IMD's only market, against every dip-buyer, through the better part of a day'. Measured with the kept judge harness: after twelve paced hours at 0.8 the paid price is 0.8864 (0.99^12), the quoted fee is 500 bps (the cap, since 50,000 is half the 100,000 floor base), and 50,000 imdUSD takes 53,588 IMD worth $53,588 at the pre-fall price: +7.2%. Break-even is 5.1 paced hours at the cap, 1.6 hours for a burn of 2% of the fee base (fee 1.5%), and the gain rate is 1% of redeemed volume per paced hour at any fee thereafter. The on-chain cost of the hold is fixed and small: pushing the $2.3M-a-side full-range pool down 20% sells 1/sqrt(0.8) - 1 = 11.8% of its IMD (about $271k at the honest price) for about $243k of ETH, and buying it back at the end returns the slippage, so the round trip costs about 2 x 1% of $271k = $5.4k in pool fees plus at most 22 attestations (under 22 IMD; the Treasury's fall trigger buys the first). The only cost that scales with the hold is dip-buying: every dollar of IMD bought from the pool at 20% off costs the attacker 20 cents to counter. So the hold is profitable at twelve hours whenever dip-buyers take less than ($72k - $5k) / 0.2 = $335k over twelve hours at LINE $1M of eligible debt (7.2% x $1M), and at 22 hours whenever they take less than $900k. Those are market assumptions the code does not enforce, and the recorded premise states no number for them. Whether the rate is 1%, 0.5% or 5% only moves the hours; it never moves the gain per hour below the rate times the eligible volume, because the paid price is a function of elapsed time alone. The right bound is therefore one on the redeemed VOLUME per paced interval, not only on the price: for example, cap the imdUSD `cash` may burn per PACE_INTERVAL at a fraction of the paced supply (2% gives at most $20k an hour of volume, so a 22-hour hold earns at most about $50k against $1M of eligible debt and a $20k-an-hour dip-buying rate already defeats it), or price the payout at the higher of the attested price and the feed's epoch anchor for the first FRESH_DEBT_WINDOW after a fall. Either keeps honest redemptions open at the honest price and is a design decision for the requester; the finding is that the stated economic reason is unquantified and the on-chain cost of the hold is about $5k.","line":364,"path":"src/CDPVault.sol","reproduction":"Kept harness (test/payout-vault-panel/judge_high_HeldDownPoolRedemption.t.sol): BOOK at 200% with 99,500 imdUSD of debt, HOLDER with 100,000 imdUSD held a day, feeds at $1. Set the feeds to 0.8, then twelve times: advance an hour, re-attest 0.8, pace(). Expected per the recorded premise: a hold shorter than 'the better part of a day' is not profitable. Actual: payoutPrice() = 0.886384871716129283; redemptionFeeBps(50,000e18) = 500; cash(50,000e18, 0, BOOK) pays 53,588.46 IMD, worth $53,588 at the pre-fall price for $50,000 burned (test/scratch/TwelveHourHold.t.sol, 'twelve-hour hold pays more than burned: 53588459726343563414790 > 50000000000000000000000'). On-chain cost of the twelve-hour hold: about $5.4k of pool fees for the 11.8% push and its unwind, plus at most twelve attestations.","severity":"medium","snippet":"    uint256 public constant PAYOUT_PRICE_FALL_BPS_PER_HOUR = 100;","title":"The 1%-an-hour payout fall bounds the attacker's gain at 1% of redeemed volume per paced hour, and the stated reason (the hold's cost exceeds the gain) is not supported by the on-chain cost: a twelve-"},{"citation":"resolved","description":"Question 1, the rise direction. The accepted low (payout vault panel #2) states the bound as one window: a 20% push written at once, decaying 1% a paced hour, underpays redeemers for about 18 hours. The feed admits the cap per epoch from the epoch's anchor, so a pool held up through two consecutive windows writes 1.2 x 1.2 = 1.44 and through three 1.728 (the sweep oracle panel's own 'up' walk reaches 3.58 in six), and `_pacedPrice` rises to each at once. Released, the paid price decays at 1% a paced hour from that level: ln(1.44)/ln(1/0.99) = 36 paced hours for two windows, 55 for three, 127 for the six-rung walk. During that time every redemption is paid at the pushed price (never overpaid, never below the attested price), which is the peg defence blocked, not funds taken. The direction and the cost model are the ones already accepted; the recorded bound is the single-window figure and should state that it compounds with the hold's length, so an operator reading 'about 18 hours' after a two-hour pump expects the wrong recovery time. No code change is needed if the bound is restated; if a limit is wanted, the paced price's rise could be bounded by the feed's own epoch cap per PACE_INTERVAL (20%), which still follows any honest rally the feed can carry.","line":361,"path":"src/CDPVault.sol","reproduction":"Kept harness (judge_high_HeldDownPoolRedemption.t.sol, second test). Set the feeds to 1.2 and pace (hour 1: payoutPrice 1.2); set them to 1.44 and pace (hour 2: payoutPrice 1.44, within the second epoch's 20% of its 1.2 anchor); set them back to 1.0 and pace hourly. Expected per the recorded bound: payoutPrice back at 1.0 within about 18 paced hours. Actual: 1.44 x 0.99^h reaches 1.0 only at h = 37 (0.99^36 x 1.44 = 1.0027, 0.99^37 x 1.44 = 0.9927); cash(50,000e18, 0, BOOK) at hour 18 is paid at 1.44 x 0.99^18 = 1.20, so 50,000 imdUSD takes about 39,530 IMD rather than 47,500.","severity":"low","snippet":"    /// push, less 1% a paced hour, until it has decayed: about 18 hours for 20%, for the cost of the push","title":"The pump bound 'about 18 hours for 20%' is per feed window: consecutive epochs compound, so two windows (44%) underpay every redemption for about 36 paced hours and three (73%) for 55"},{"citation":"resolved","description":"Question 2 and question 7. `_recentlyMinted` returns the whole record while now - mintedAt < 12 hours, and `draw` moves `mintedAt` toward the present by the new tranche's share of the record (principal-time is conserved), so a record holding two equal tranches drawn at t0 and t0 + 11 h is dated t0 + 5.5 h and stays fresh whole until t0 + 17.5 h: the first tranche is then seventeen hours old, the paced debt has followed it for seventeen hours (ten per cent of the larger of the paced debt and the floor per hour, compounding), and a self-redemption of the whole record nets all of it out of CANCELLED_PRE_SLOT. The claim 'at most that long' is therefore not exact. I checked the consequence and found none that the design does not already accept: the clamp also takes the minimum with the live debt the cancellation has lowered, so the paced debt can exceed the correct figure only by the lag the follow had not absorbed, and what that buys (the paced figure staying attached to the younger tranche instead of restarting at the follow rate) equals what the same borrower gets by holding the older tranche in a second position and repaying it after earning, which ParameterizedVault.backedDebt documents as the accepted point-in-time ceiling. At wage 0 nothing reads the paced debt. The sentence should say the record's mean age is under the window, or the netting should use the tranche-aware figure if one is ever kept.","line":932,"path":"src/CDPVault.sol","reproduction":"Position X draws 500,000 at t0 (record: mintedAt t0, recentlyMinted 500,000) and 500,000 at t0 + 11 h (mintedAt t0 + 5.5 h, recentlyMinted 1,000,000). At t0 + 12 h + 1 s, X (eligible at 170% at the attested price) redeems 500,000 of its own imdUSD against itself: _recentlyMinted(X) is 1,000,000 (6.5 h < 12 h), `recent > own` nets the whole 500,000, CANCELLED_PRE stays 0 and the paced debt is clamped only to min(pacedAtStart, live). Expected by the NatSpec: the 500,000 drawn twelve hours earlier is seasoned and its cancellation lowers the paced debt at once, as it does when the two tranches sit in two positions (the first position's record has aged out, CANCELLED_PRE = 500,000). Actual: the paced debt is unchanged by the cancellation.","severity":"info","snippet":"    /// own record, `_recentlyMinted`) is, when cancelled, booked nowhere, since the paced debt has had at most","title":"_tallyPrincipalRetired's NatSpec says the paced debt 'has had at most' FRESH_DEBT_WINDOW to follow the netted principal; the amount-weighted record keeps a tranche fresh for up to about twice the wind"}],"hash":"2fef1ea787297a9273c14646b645714f9f882c767cfe7a3a345afef5ec19ef6d","nodeId":"bdce715b-dcb5-466c-af43-5f0828167372","outcome":"completed","summary":"The review is complete. The findings file is written and validated, the only working-tree changes are the findings file and test/scratch/ (both ignored by git), and the proof test fails on this commit for the stated reason.\n\n## Result\n\n`.imd-findings.json` holds four findings: one high with a Foundry proof, one medium, one low, one info. All four snippets match the tree at e4baedf.\n\n**High, `bite` at a held-down price (src/CDPVault.sol:1354).** The paced payout price protects `cash` from a pool held down through the feed window, but `bite` still prices the seizure at the attested price alone. Two fresh-cap rungs (0.8, then 0.64 an hour later), a mark, and a six-hour grace held at 0.64 let a liquidator seize 1.875 IMD per imdUSD burned. The sweep oracle panel accepted this walk with the prize bounded at \"20% of debt repaid, at most $200k on the whole LINE\". That bound prices the bonus at the honest price. Measured in test/scratch/HeldDownLiquidation.t.sol: 480,078 imdUSD burned seizes sIMD worth $900,146 at the pre-fall price, and the liquidator keeps $885,144. An eight-hour hold costs about $11.5k in pool fees plus dip-buyer absorption. Smallest fix: price the seizure at max(attested, paced payout price), with the rate chosen against liquidator profitability after an honest fall.\n\n**Medium, the 1%-an-hour bound's reason (line 364).** The arithmetic holds exactly as stated. What is unsupported is the premise that the hold's cost exceeds the gain. Measured with the kept judge harness: twelve paced hours pays $53,588 for $50,000 burned at the 5% fee cap. The on-chain cost of a 20% push and unwind is about $5.4k in pool fees. The attacker's gain is the rate times eligible volume per hour at any rate, so the right bound is on redeemed volume per interval, not only on price.\n\n**Low, the pump bound compounds (line 361).** \"About 18 hours for 20%\" is one window. Two consecutive epochs write 1.44 and block honest-price redemptions for about 36 paced hours.\n\n**Info, NatSpec at line 932.** The weighted record keeps a tranche fresh up to about 17 hours, so \"at most\" the window is not exact. I traced the consequence and it collapses into the already-accepted point-in-time ceiling, so nothing to take at wage 0.\n\n## Answers where nothing is wrong\n\n- **Clamp and netting (Q2).** Every ordering of draw, wipe, cash, bite and cover I traced keeps the paced debt at or under the live debt. The wipe tally being per transaction rather than per position lets one position's seasoned debt be handed to another in one call, but the total is unchanged and the point-in-time ceiling already accepts it.\n- **Paced figures across the system (Q3).** A relay bundling an update with a pace or a cash cannot pay above the paced price, since the rise is written at once and the fall waits on the clock. A stale or diverged window holds both the backing and the payout price without consuming their interval. A Chainlink outage reads as stale and halts gated paths.\n- **Launch window (Q4).** verifySeeded checks both references and both bands. The seeded paced supply is capped at the floor. The first `_pricePaced` is the first usable price, with no supply to redeem against. The halts are the three feed lifetimes, the two-hour Chainlink age and the 5% skew, each recovering with one purchased attestation.\n- **Governance, Treasury, deploy (Q6).** No regression found. The Sepolia Chainlink constant as committed is rewritten by plan.py and refused by the script otherwise. The initcode test passes.\n\n## Coverage\n\nRead in full: CDPVault, ParameterizedVault, Treasury, SwarmFeed, PriceFeed, SpotFeed, NhiFeed, UsdPriceFeed, SharePriceFeed, SwarmRelay, TransientReentrancyGuard, ImdUSD, DeploymentConfig, DeployMainnet, DeployPreflight, runbook section 7 and 7b, the payout panel and sweep-2 resolutions. Read only in part: Parameters (apply and drip path), OracleAsker (not read), plan.py (constant rewriting only). Not reached: the pinned bodies and check-bodies.mjs, the full test suite.","treeHash":null,"usage":{"cachedInputTokens":3689856,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":65281,"runtime":"claude","turns":38,"wallClockMs":1009392}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"af9a875696459139","findings":[{"citation":"resolved","description":"The final sweep 2 high (a pool held down through the feed's window paid a redeemer the whole fall) was answered by pacing the price a redemption is PAID at (max(attested, paced), falling 1% a paced hour), and the resolution's bound rests on the claim that a pool cannot be held 20% down on its only market for the better part of a day at a cost below the gain. The same hold reaches the vault through `bite`, which is unpaced on both sides: health is checked and the seizure priced at the attested value (`_requireFreshFeeds`, line 1354), both feeds read the one pool, and the feeds admit a 20% step from a fresh anchor (40% after two silent hours). A pool pushed 20% down and re-attested hourly makes every position under 170/0.8 = 212.5% CR (the whole honest book at the runbook's ~175-200%) underwater; after one window and the grace (6h at NHI >= 0.85, `_lull`) a liquidator burns D imdUSD and receives 1.2 D / 0.8 = 1.5 D of IMD at the pre-hold value (1.475 D keeping the marker's share, 1.45 D not). The hold is about seven hours, not twenty-two, and it pays 45-50% of the liquidated debt, not 1% an hour less a 5% fee; nothing in the protocol shortens it (resecure and the Treasury's fall trigger only re-price and refresh at the held value; the fall trigger buys the attestation that anchors it). Reachable with the constants as committed by anyone able to hold the pool: at LINE $1M the exposed debt is every position the hold takes under mat, bounded only by the liquidator's imdUSD and the pool's depth to sell into; the borrower is the party that loses (the position is healthy at the honest price), with the protocol's cut and marker's share paid on top. This is the oracle-manipulation surface the grace was sized for, so it may be ACCEPTED as design, but then the resolution's cost argument (most of a day) and `cash`'s comment ('a pool held down is paid the hours it is held') do not cover the vault's richer path, and the record should price the seven-hour hold against the ~$5k of pool fees and the dip-buying it must absorb. Smallest fix if it is not accepted: price the seizure (not the health check) at the same paced price `cash` pays, `Math.mulDiv(debtToRepay, 1.2e18, _payoutPrice(price))`, so a hold pays a liquidator the bonus plus 1% a paced hour exactly as it pays a redeemer; the cost, to be decided, is that after a real 20% fall the liquidator's payout (1.2 x 0.8 / 0.99^h) is under 1.05 for the first ~9 paced hours, delaying honest liquidations by that long unless the mark's grace or `mat` is re-derived for it (docs/PARAMETERS-2026-10-05.md sizes mat for grace plus the lag).","line":1354,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// The payout price is paced for `cash` (1% an hour), so a pool held 20% down pays a redeemer only the hours it is\n// held. `bite` seizes at the ATTESTED price with no pacing: the same hold, through one feed window plus the grace\n// (six hours at NHI >= 0.85), liquidates every position under 212.5% CR and pays the liquidator 1.2 / 0.8 = 1.5x\n// the debt burned, measured at the pre-hold price. The hold the payout vault panel's resolution prices at \"the\n// better part of a day\" is about seven hours on this path, and pays 47.5% of the liquidated debt, not 1% an hour.\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Math} from \"@openzeppelin/contracts/utils/math/Math.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract HlFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 hours;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external view returns (bool) {\n        return block.timestamp - updatedAt > maxAge;\n    }\n}\n\ncontract HlAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract HeldDownLiquidationTest is Test {\n    address private constant BOOK = address(0xB00C);\n    address private constant HOLDER = address(0x401D);\n    uint256 private constant DOLLAR = uint256(1 ether) * 1e18 / 2000 ether;\n\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    ImdUSD private stable;\n    HlFeed private primary;\n    HlFeed private health;\n    HlFeed private spot;\n    uint256 private imdEth = DOLLAR;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new HlAggregator()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        primary = new HlFeed(DOLLAR);\n        health = new HlFeed(0.85 ether); // mat 170, grace 6h\n        spot = new HlFeed(DOLLAR);\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(health), address(spot)\n        );\n        stable = vault.stablecoin();\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(BOOK, 200_000 ether);\n        imd.mint(HOLDER, 300_000 ether);\n        vm.stopPrank();\n        vm.startPrank(BOOK);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(199_000 ether);\n        vault.draw(99_500 ether); // 200%: healthy by 30 points\n        vm.stopPrank();\n        vm.startPrank(HOLDER);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(300_000 ether);\n        vault.draw(100_000 ether);\n        vm.stopPrank();\n        for (uint256 i; i < 24; ++i) {\n            _hour();\n        }\n    }\n\n    function _next(uint256 seconds_) private {\n        vm.warp(block.timestamp + seconds_);\n        vm.roll(block.number + 1 + seconds_ / 12);\n        primary.set(imdEth);\n        spot.set(imdEth);\n        health.set(0.85 ether);\n    }\n\n    function _hour() private {\n        _next(1 hours);\n        vault.pace();\n    }\n\n    function test_aPoolHeldDownThroughTheGraceLiquidatesAHealthyPositionAtOneAndAHalfTimesTheDebt() public {\n        uint256 preFall = DOLLAR;\n        imdEth = DOLLAR * 80 / 100; // one step the feeds accept, both feeds on the one pool\n        _next(12);\n        vm.prank(HOLDER);\n        vault.bark(BOOK); // 199,000 x 0.8 / 99,500 = 160% < 170\n        for (uint256 i; i < 6; ++i) {\n            _hour(); // the grace at NHI >= 0.85, the pool re-attested hourly\n        }\n        assertGe(vault.payoutPrice(), 0.94 ether, \"cash would be paid at the paced price, down at most 6%\");\n        uint256 before = imd.balanceOf(HOLDER);\n        vm.prank(HOLDER);\n        vault.bite(BOOK, 50_000 ether);\n        uint256 seized = imd.balanceOf(HOLDER) - before;\n        uint256 valueAtPreFall = Math.mulDiv(seized, preFall * 2000, 1e18);\n        // EXPECTED: a liquidation during a hold is paid at most the bonus over the PACED price, as a redemption\n        // is: 50,000 x 1.2 / 0.99^7 = 64,380 IMD, worth that at the pre-hold price. ACTUAL: 73,750 IMD (75,000\n        // seized at the attested low, less the protocol's cut) for 50,000 imdUSD, after a seven-hour hold.\n        assertLe(valueAtPreFall, 65_000 ether, \"a held-down pool pays the liquidator the whole fall at once\");\n    }\n}","reproduction":"test/scratch/HeldDownLiquidation.t.sol. BOOK locks 199,000 IMD at $1 (IMD/ETH 5e14 wei, ETH/USD 2000) and draws 99,500 (200% CR, 30 points above mat 170); HOLDER locks 300,000 and draws 100,000; 24 hourly pacings. Primary and spot both step to 0.8x (the feeds' one-window allowance); HOLDER barks BOOK (159,200 / 99,500 = 160% < 170). Six hourly re-attestations at the held price (the grace at NHI 0.85), then HOLDER bites 50,000. EXPECTED: at most the bonus over the paced price, 50,000 x 1.2 / 0.99^7 = 64,380 IMD, worth $64,380 at the pre-hold price. ACTUAL: HOLDER receives 73,750 IMD (75,000 seized less the protocol's 1,250 cut), worth $73,750 at the pre-hold price, for 50,000 imdUSD: 47.5% taken from a borrower healthy at the honest price after a seven-hour hold. `payoutPrice()` at the same moment is 0.932 (cash would pay 50,000 x 0.95 / 0.932 = 50,965 IMD).","severity":"medium","snippet":"        uint256 collateralSeized = Math.mulDiv(debtToRepay, (100 + CHOP_PERCENT) * 1e16, price);","title":"bite seizes at the attested price with no pacing: a pool held 20% down through one window plus the grace liquidates healthy positions at 1.5x the debt burned, a shorter and richer hold than the paced "},{"citation":"resolved","description":"_tallyPrincipalRetired (73191e0) nets a position's whole _recentlyMinted record out of a cancellation on the reasoning that \"the paced debt has had at most [FRESH_DEBT_WINDOW] to follow it\" (NatSpec 931-935, resolution of payout vault panel #3). But the paced debt follows at FOLLOW_BPS_PER_HOUR = 10% an hour of max(paced, 100,000), compounding per pacing: a draw of 100% of the book is followed in full in about 7.3 paced hours and one of 10% in one hour. Principal between ~1 and 12 hours old is therefore both 'recent' for the netting and fully counted in _debtPaced. Cancelling it (cash, bite, cover) books nothing to CANCELLED_PRE_SLOT, _clampPacedDebt leaves _debtPaced at min(pacedAtStart, live), and the slot the cancelled loan occupied in the paced figure is filled by whatever debt was drawn since, however fresh. This is exactly the sequence the final sweep 2 low (1) closed on a3aa9e4 ('debt cancelled by a redemption and drawn again by someone else backs nothing until it has been held', CDPVault 311-313, ParameterizedVault 238-241 and 265-266): the fix for payout vault panel #3 reopened it for cancelled loans under twelve hours old. Reachable with the constants as committed, from any two positions; the only consumer of _debtPaced is ParameterizedVault.backedDebt -> earnLine, and WAGE_WAD is 0 at launch, so nothing is takeable today (low, the rating the earlier rounds gave the same sequence). Once a wage is set behind the timelock it is the sweep-panel high's round trip at one-block holding time: earnMat 25% of the fresh draw minted as work against debt that is wiped the next block. Smallest fix: net out of a cancellation only this transaction's own mint (the `own` term) as a3aa9e4 did, accepting the bounded churn (the paced debt recovers at 10% an hour) that panel #3 rated low; or, if that churn must stay closed, net out only principal whose record is younger than PACE_INTERVAL (one hour), which bounds what can be counted at once to one hour of follow (10% of the book) instead of the whole book. The NatSpec at 931-935 ('the paced debt has had at most that long to follow it') and the three 'backs nothing until it has been held' sentences claim a property the code no longer has.","line":953,"path":"src/CDPVault.sol","reproduction":"test/scratch/NettingGap.t.sol, test_cancellingAnElevenHourOldFollowedLoanLetsAOneBlockOldDrawCountAtOnce. Book: BOOK locks 199,000 IMD at $1 and draws 99,500, paced hourly for 24h (paced debt 99,500). P1 locks 340,000, draws 100,000, 11 hourly pacings (paced debt 199,500: P1's loan is followed in full). P1 frees 130,000 (CR 210%, a candidate). P2 locks 340,000 and draws 100,000 (paced debt still 199,500). Next block (12s) P1 calls cash(100,000, 0, P1) with the Treasury's reserve empty, cancelling its eleven-hour-old loan. EXPECTED: paced debt and backedDebt back at the seasoned 99,500 (+ P1's ~5.6 imdUSD of fees), P2's twelve-second-old loan uncounted. ACTUAL: paced debt 199,505.58e18 and backedDebt 199,505.58e18; P2's loan counts in full. On c90e8d9 (the clamp of final sweep 2 without the netting, where every cancellation but the transaction's own mint was booked to CANCELLED_PRE_SLOT) the same sequence clamps the paced debt to about 99,505.","severity":"low","snippet":"        if (recent > own) rest -= Math.min(rest, recent - own);","title":"Fresh-principal netting treats any principal under 12h as unfollowed, so cancelling an already-followed loan lets a one-block-old draw count for backedDebt at once"},{"citation":"resolved","description":"The resolution of payout vault panel #3 states the residual cost of lowering the paced debt by a self-cancellation as holding the debt twelve hours (FRESH_DEBT_WINDOW), since only principal older than the record's window is booked to CANCELLED_PRE_SLOT and clamps _debtPaced. The bound does not hold. _reduceDebt retires youngest-first and conserves principal-time (lines 1600-1604): after a wipe of all but r of a tranche X drawn t ago, the remainder is dated t*X/r back and, once that exceeds the window, the record ages out whole ('A record whose remaining debt would be dated outside the window simply ages out'). With X/r >= 12 a one-hour-old remainder reads as seasoned; with X/r >= 3,600 a twelve-second-old one does. Its cancellation is then booked in full to CANCELLED_PRE_SLOT and _clampPacedDebt subtracts it from the paced debt the transaction began with, although the follow had absorbed at most one step (10% of max(paced, floor)) of X. The paced debt lands below the untouched seasoned book, and with it backedDebt and earnLine, recovering at 10% an hour. Only the work ceiling reads it and WAGE_WAD is 0 at launch (nothing blocked today: low); at a wage it is a repeatable, hour-priced denial of earn, cheaper than the twelve hours the record states by a factor of twelve (or 3,600). Cost to the attacker: one hour of duty on X (about $3 on 600,000) and the collateral for it; the self-redemption's fee stays in its own position. Smallest fix: bound what a cancellation may clamp by what the follow could have counted of it: keep a per-position 'last drawn at' timestamp (not the amount-weighted mintedAt) and book principal of a position drawn within FRESH_DEBT_WINDOW as recent whatever its record says, or cap the clamp at pacedAtStart - (liveAtStart - debtAtTxStartOfTheRecord). Otherwise restate the accepted bound as one hour / one block in the record and the NatSpec.","line":954,"path":"src/CDPVault.sol","reproduction":"test/scratch/NettingGap.t.sol, test_aOneHourOldRemainderAgedOutByTheWipeClampsThePacedDebtUnderTheSeasonedBook. Book: BOOK 199,000 IMD / 99,500 imdUSD, paced 24h (paced debt 99,500). P1 locks 1,200,000 IMD and draws 600,000 (paced debt unchanged). One hour later pace() (paced debt 109,500: one 10% step). P1 wipes 551,000: the 49,000 remainder is dated 12.24h back and the record ages out (recentlyMinted 0). P1 frees 1,097,000 (CR 210%). Next block P1 calls cash(49,000, 0, P1) (reserve empty). EXPECTED per the record: the paced debt cannot be lowered under the seasoned book without holding the debt twelve hours, so paced >= 99,500 and backedDebt >= 99,500 while totalDebt is 99,503. ACTUAL: paced debt 60,536.5e18 and backedDebt 60,536.5e18 after a one-hour hold.","severity":"low","snippet":"        if (rest != 0) _transientAdd(CANCELLED_PRE_SLOT, rest);","title":"A remainder aged out by the record's conserved principal-time is booked as pre-existing, so the paced debt is driven under the seasoned book after a one-hour (or one-block) hold, not twelve"},{"citation":"resolved","description":"Three places state the property the final sweep 2 low (1) was fixed to provide: ParameterizedVault.sol 238-241 and 265-266 ('debt cancelled ... backs nothing even if the same amount is drawn again', 'cancelling another position's debt and drawing as much backs nothing until the new debt has been held') and CDPVault.sol 311-313 ('debt cancelled by a redemption or a liquidation and drawn again by someone else backs nothing until it has been held'). CDPVault.sol 931-935 (_tallyPrincipalRetired) gives the reason the netting is safe: 'the paced debt has had at most that long [FRESH_DEBT_WINDOW] to follow it'. With the follow at 10% an hour, a loan under twelve hours old is usually followed in full, its cancellation is netted out, and debt drawn one block earlier by another position counts at once (the first finding's reproduction). Either the netting is bounded (see the first finding's fix) or these sentences are reworded to the property the code has: cancelled principal younger than twelve hours does not lower the paced debt, so debt drawn against it counts as soon as the paced figure has room. The record for payout vault panel #3 should state the same, and its 'twelve hours' cost for lowering the paced debt should read one hour (second finding).","line":265,"path":"src/ParameterizedVault.sol","reproduction":"test/scratch/NettingGap.t.sol, both tests: after the sequences in the first two findings the paced debt is 199,505 with a twelve-second-old loan counted (expected 99,505 per the quoted sentences) and 60,536 after a one-hour hold (expected not under 99,500 without a twelve-hour hold per the record).","severity":"info","snippet":"        // at once, so debt drawn to lift the ceiling must be held for hours and debt cancelled this transaction\n        // (by a redemption, a liquidation or cover) backs nothing even if the same amount is drawn again.","title":"NatSpec claims cancelled-and-redrawn debt 'backs nothing until it has been held' and that the netting window is 'at most that long to follow it'; neither holds after 73191e0"}],"hash":"3dd7b41e954b758b47368a0031a2517405fafc445368b753fac472ed779dc176","nodeId":"a3d3b204-765b-46a5-8fd1-3df8a1791fbf","outcome":"completed","summary":"All findings are recorded in `.imd-findings.json` (one medium, two lows, one info), each with a reproduction that fails on this commit, and the scratch tests remain under `test/scratch/`. Here is the review, question by question.\n\n## Findings\n\n- **Medium, `src/CDPVault.sol:1354` (bite).** The payout-price pacing covers only `cash`. Liquidation prices the seizure at the attested value with no pacing, and both feeds read the one pool. A pool held 20% down for one window plus the grace (six hours at NHI 0.85) makes every position under 212.5% CR underwater and pays the liquidator 1.5x the debt burned at the pre-hold price. Proof: a 200% position is barked, the pool is re-attested hourly for six hours, and a 50,000 imdUSD bite returns IMD worth $73,750 at the honest price. The record's cost argument for the hold (\"the better part of a day\") covers a seven-hour path that pays 47.5% of liquidated debt instead of 1% an hour. This may be accepted as the oracle risk grace was sized for, but then the record should say so and price it. The fix, if any, is a design call: seize at the paced price, at the cost of delaying honest liquidations after a real fall by about nine paced hours.\n- **Low, `src/CDPVault.sol:953` (the netting).** The netting treats any principal under twelve hours old as unfollowed, but the follow absorbs a draw of the whole book in about seven hours. Cancelling an eleven-hour-old, fully followed loan books nothing, and a loan drawn one block earlier by another position counts in full for backedDebt. This reopens the final sweep 2 low that the clamp was written for. Nothing is takeable at wage 0.\n- **Low, `src/CDPVault.sol:954` (the clamp).** The resolution's stated cost for driving the paced debt down, holding the debt twelve hours, does not hold. Youngest-first retirement conserves principal-time, so a 600,000 draw wiped down to 49,000 after one hour leaves a remainder dated 12.2 hours back. Cancelling it clamps the paced debt from 109,500 to 60,536, under the untouched 99,500 book.\n- **Info, `src/ParameterizedVault.sol:265`.** Three NatSpec sentences claim cancelled-and-redrawn debt \"backs nothing until it has been held\", and the tally function claims the paced debt \"has had at most that long to follow\" recent principal. The first two findings show the code no longer has either property.\n\n## The questions where nothing is wrong\n\n**1. The payout price.** The pacing arithmetic is right: hourly pacing gives 0.99 per hour, every-block pacing gives e^-0.01, and sparser pacing is slower, so 1% an hour is the maximum fall. For `cash`, a hold-down pays 0.95 / 0.99^h minus one, so break-even is about five hours at cap-fee volumes and the whole 20% at 22 hours; sub-9,000 tranches pay near the floor and break even at two to three hours, but the stored base rate reaches the cap within about eight such redemptions. The push itself costs about 11.8% of the IMD side in round-trip fees plus every dip-buyer absorbed at minus 20%. A rise is written at once and never overpays; a 20% push blocks about 18 hours as stated, and a 40% stale-window step blocks about 34, which the constant's NatSpec does not mention. Fall-and-rise sequences do not accumulate, because a rise resets the paced price. After a real fall the backing scale is valued at the attested price while the payout price lags, so redeemers are underpaid by exactly the paced-to-attested ratio, as the NatSpec says, with no double count. The answer to \"does anything pay for the hold\" is the first finding.\n\n**2. The clamp.** Beyond the two lows above, the transient tallies stay consistent across every ordering I traced: a wipe and redraw of this transaction's own mint leaves the debt figure unchanged, the double-restore of the supply inside a transaction only lowers what is paid, and the clamp is bounded by the live debt in every path.\n\n**3. Across the system.** A donation to the Treasury lifts the payout by at most two points an hour and is itself the first IMD paid out, so it ca","treeHash":null,"usage":{"cachedInputTokens":3203198,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":78973,"runtime":"claude","turns":37,"wallClockMs":1203439}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3987a51ff810f3b9","findings":[{"citation":"resolved","description":"The payout-price fix (PAYOUT_PRICE_FALL_BPS_PER_HOUR = 100) rests on the claim that a pool cannot be held 20% down on its only market for most of a day at a cost below the gain, where the gain is 1% of redeemed volume per paced hour. That reasoning does not cover the other consumer of the same held price: bite seizes debtToRepay x 1.2 / price at the ATTESTED price with no pacing, after a grace of lull() (6 hours at NHI >= 0.85) and within tail() (1 hour). A 20% step is inside the feed's fresh allowance, both feeds read the one pool so the skew check passes, the Treasury's fall trigger buys the first attestation, and the keeper's hourly relay keeps the low fresh. Every position whose honest CR is under 212.5% (170 / 0.8) is unhealthy at 0.8P, can be marked at hour 0 and bitten between hours 6 and 7. The seizure is worth 1.2 / 0.8 = 150% of the debt repaid at the honest price, so the attacker nets about 50% of the debt bitten (less the 4% of bonus paid to marker and Treasury, which the attacker can be) against a hold of about seven hours rather than the 22 the payout price needs for a 20% redemption gain. At LINE $1M with a book at 200% CR that is up to about $500k of borrowers' sIMD for the cost of selling ~12% of the pool's IMD side (~$270k, 1% fee each way) and absorbing dip-buyers for seven hours. docs/PARAMETERS-2026-10-05.md prices only the RISE walk (over-borrowing) and its liquidation floor models an honest crash; docs/AUDIT-FINAL-SWEEP-2 priced the fall route only for cash. The records state no accepted bound for a held-down liquidation, so the paced-payout Resolution's cost claim is not shown to hold for the route that pays most. Smallest fix: value the seizure in bite (and the health check in bark/bite) at max(attested, paced payout price) the way cash does, so a held low must decay through the paced price before it moves collateral; or lengthen the mark lifetime so a mark taken at a price more than X% below the paced price cannot be bitten until the paced price has followed.","line":1354,"path":"src/CDPVault.sol","reproduction":"test/scratch/HeldDownLiquidation.t.sol (run on e4baedf, passes as a demonstration): ParameterizedVault over MockIMD at $1, NHI 0.85 (mat 170, lull 6h). BOOK locks 199,000 and draws 99,500 (200% CR at the honest price); HOLDER holds 100,000 imdUSD drawn earlier at the honest price; 24 hourly pacings. Both feeds step to 0.80 (one fresh-anchor step). Next block HOLDER calls bark(BOOK): accepted, CR 160 < 170. Six hourly relays and pacings at 0.80. Next block HOLDER calls bite(BOOK, 99,500e18): collateralSeized = 99,500 x 1.2 / 0.8 = 149,250 IMD; HOLDER (also the marker) receives 146,762 IMD, worth $146,762 at the pre-fall price, for 99,500 imdUSD burned: a $47,262 gain on one position, 47.5% of the debt bitten, after a seven-hour hold. BOOK keeps 49,750 of 199,000 IMD. EXPECTED under the Resolution's cost claim for a 20% hold: a gain of about 1% of volume per paced hour, so at most about 7% after seven hours. ACTUAL: 47.5%, none of it paced. At LINE $1M against a book at 200% CR the same hold reaches about $475k. Reachable with the constants as committed.","severity":"medium","snippet":"        uint256 collateralSeized = Math.mulDiv(debtToRepay, (100 + CHOP_PERCENT) * 1e16, price);","title":"Hold-down route the paced payout price does not bound: a pool held 20% down for one grace period liquidates every position under 212% honest CR at the held price, a 50% gain on the debt bitten"},{"citation":"resolved","description":"The constant's NatSpec (and the Resolution of docs/AUDIT-PAYOUT-VAULT-PANEL-2026-10-09.md, 'the fee eats the first five hours of any hold') states the accepted bound of a held-down pool as 1% of redeemed volume per paced hour after a five-hour fee. The fee is REDEMPTION_FEE_FLOOR_BPS (50) plus the burn's share of the fee base over the divisor, capped at 500: it reaches 5% only for a burn of about 9% of the fee base in one go. A burn of 0.5% of the fee base pays 75 bps, so with the paid price at 0.99 x the pre-fall price after ONE paced hour a redeemer takes 0.9925 / 0.99 = 1.0025 of the pre-fall value, and after two hours 1.0127. The direction and the per-hour rate the fix claims are right; the five-hour break-even is not, and the sentence is the whole statement of what the rate buys. What bounds a sustained drain is the base rate's ratchet (12-hour half-life), not the five hours: hourly burns small enough to stay under a 2% fee are about 0.18% of the fee base each, so a 22-hour hold against a $1M fee base yields under $4,000, which is the figure the NatSpec should rest on. Smallest fix: replace 'after the fee has eaten the first five' with the actual bound (break-even at fee / 1% paced hours, under one hour at the floor fee; a sustained drain limited by the base-rate ratchet to a fraction of a percent of the fee base over a full decay), in the constant's NatSpec, cash's comment and record 26.","line":353,"path":"src/CDPVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n// The constant's NatSpec (CDPVault.sol:354) and cash's comment say a held-down pool pays a redeemer \"1% an hour\n// here, after the fee has eaten the first five\". The fee is 50 bps plus the burn's share of the fee base over the\n// divisor: it reaches the 5% cap only for a burn of about 9% of the fee base. A small burn pays about 0.5-0.75%,\n// so a pool held down ONE paced hour already pays it more at the pre-fall price than it burned, and a two-hour\n// hold pays 1.3% over. The bound stated for the accepted item (\"the fee eats the first five\") does not hold.\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Math} from \"@openzeppelin/contracts/utils/math/Math.sol\";\nimport {ParameterizedVault} from \"src/ParameterizedVault.sol\";\nimport {ImdUSD} from \"src/ImdUSD.sol\";\nimport {MockIMD} from \"src/MockIMD.sol\";\nimport {TreasuryFactory} from \"src/TreasuryFactory.sol\";\nimport {ISwarmFeed} from \"src/interfaces/ISwarmFeed.sol\";\nimport {APPROVED_OPERATOR, CHAINLINK_ETH_USD, TREASURY_FACTORY} from \"src/DeploymentConfig.sol\";\n\ncontract SbFeed is ISwarmFeed {\n    uint256 public constant maxAge = 1 hours;\n    uint256 private value;\n    uint64 private updatedAt;\n\n    constructor(uint256 v) {\n        set(v);\n    }\n\n    function set(uint256 v) public {\n        value = v;\n        updatedAt = uint64(block.timestamp);\n    }\n\n    function latestValue() external view returns (uint256, uint64) {\n        return (value, updatedAt);\n    }\n\n    function isStale() external view returns (bool) {\n        return block.timestamp - updatedAt > maxAge;\n    }\n}\n\ncontract SbAggregator {\n    function decimals() external pure returns (uint8) {\n        return 8;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 2000e8, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract SmallRedemptionBreakEvenTest is Test {\n    address private constant BOOK = address(0xB00C);\n    address private constant HOLDER = address(0x401D);\n    uint256 private constant DOLLAR = uint256(1 ether) * 1e18 / 2000 ether; // IMD/ETH at $1\n\n    MockIMD private imd;\n    ParameterizedVault private vault;\n    ImdUSD private stable;\n    SbFeed private primary;\n    SbFeed private health;\n    SbFeed private spot;\n    uint256 private imdEth = DOLLAR;\n\n    function setUp() public {\n        if (TREASURY_FACTORY.code.length == 0) vm.etch(TREASURY_FACTORY, address(new TreasuryFactory()).code);\n        vm.etch(CHAINLINK_ETH_USD, address(new SbAggregator()).code);\n        vm.warp(1_000_000);\n        imd = new MockIMD();\n        primary = new SbFeed(DOLLAR);\n        health = new SbFeed(0.85 ether);\n        spot = new SbFeed(DOLLAR);\n        vault = new ParameterizedVault(\n            address(imd), address(0), address(0), address(primary), address(health), address(spot)\n        );\n        stable = vault.stablecoin();\n        vm.startPrank(APPROVED_OPERATOR);\n        imd.mint(BOOK, 2_000_000 ether);\n        imd.mint(HOLDER, 300_000 ether);\n        vm.stopPrank();\n        vm.startPrank(BOOK);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(1_800_000 ether);\n        vault.draw(900_000 ether); // 200%: the candidate; a $1M book\n        vm.stopPrank();\n        vm.startPrank(HOLDER);\n        imd.approve(address(vault), type(uint256).max);\n        vault.lock(300_000 ether);\n        vault.draw(100_000 ether);\n        vm.stopPrank();\n        for (uint256 i; i < 48; ++i) {\n            _hour();\n        }\n        assertEq(vault.backingPerUnit(), 1e18, \"a par book\");\n    }\n\n    function _next(uint256 seconds_) private {\n        vm.warp(block.timestamp + seconds_);\n        vm.roll(block.number + 1 + seconds_ / 12);\n        primary.set(imdEth);\n        spot.set(imdEth);\n        health.set(0.85 ether);\n    }\n\n    function _hour() private {\n        _next(1 hours);\n        vault.pace();\n    }\n\n    /// @dev A 20% step held and paced for ONE hour; a 5,000 imdUSD burn (0.5% of the 1M fee base: fee 50 + 25 bps).\n    function test_oneHourHoldAlreadyPaysASmallRedeemerMoreThanItBurned() public {\n        uint256 preFall = DOLLAR;\n        imdEth = DOLLAR * 80 / 100;\n        _next(12);\n        _hour();\n        assertEq(vault.redemptionFeeBps(5_000 ether), 75, \"the fee on a small burn is 75 bps, not the 500 cap\");\n        vm.prank(HOLDER);\n        uint256 gemOut = vault.cash(5_000 ether, 0, BOOK);\n        uint256 valueAtPreFall = Math.mulDiv(gemOut, preFall * 2000, 1e18);\n        // EXPECTED per the NatSpec: a hold shorter than five paced hours pays at most what was burned.\n        // ACTUAL: 5,000 imdUSD takes IMD worth about 5,012 at the pre-fall price after one paced hour.\n        assertLe(valueAtPreFall, 5_000 ether, \"one paced hour pays a small redeemer more than it burned\");\n    }\n\n    /// @dev Two paced hours: 1.3% over.\n    function test_twoHourHoldPaysASmallRedeemerOnePercentOver() public {\n        uint256 preFall = DOLLAR;\n        imdEth = DOLLAR * 80 / 100;\n        _next(12);\n        _hour();\n        _hour();\n        vm.prank(HOLDER);\n        uint256 gemOut = vault.cash(5_000 ether, 0, BOOK);\n        uint256 valueAtPreFall = Math.mulDiv(gemOut, preFall * 2000, 1e18);\n        assertLe(valueAtPreFall, 5_000 ether, \"two paced hours pay a small redeemer more than it burned\");\n    }\n}","reproduction":"test/scratch/SmallRedemptionBreakEven.t.sol (attached as proof): BOOK 1,800,000 / 900,000 at 200%, HOLDER 300,000 / 100,000, 48 hourly pacings (fee base at the 1,000,000 supply, backing par). Feeds step to 0.80 and are paced once an hour later. redemptionFeeBps(5,000e18) == 75. HOLDER cash(5,000e18, 0, BOOK) returns IMD worth 5,012.63 at the pre-fall price (expected <= 5,000 per the NatSpec); after two paced hours 5,063.26. Both tests fail on e4baedf.","severity":"low","snippet":"    /// however many hours it is held (1% an hour here, after the fee has eaten the first five), the whole 20% after","title":"The paced payout price's stated bound ('the fee has eaten the first five hours') holds only for a burn at the fee cap: a small burn breaks even after one paced hour"}],"hash":"c995158ab01d8d15bee5cc8e06309fa836a9013c972b8fd5ffc607c4c90b7ea0","nodeId":"59f07c4f-f2f0-4878-bdc1-4f139e83b6a4","outcome":"completed","summary":"The findings file holds two substantiated items. Here is the review.\n\n**Outcome.** Two findings, no critical or high. The payout-price fix, the clamp and netting, the seeded supply, the transient guard, the deploy script and the runbook's launch window hold up as committed. What does not hold is the economic claim the payout-price fix rests on, in two places: its stated break-even is wrong for small burns, and the hold-down it prices is not the hold-down that pays most.\n\n**Findings (in `.imd-findings.json`)**\n\n1. **Medium, `src/CDPVault.sol:1354` (`bite`).** The paced payout price bounds `cash` only. A pool held 20% down for one grace period lets a liquidator bite every position under 212% honest CR at the held price. My test holds the feeds at 0.80 for seven hours, barks a 200% position at hour 0 and bites it at hour 6. The liquidator burns 99,500 imdUSD and receives IMD worth $146,762 at the pre-fall price, a 47.5% gain with nothing paced. At LINE $1M that is about $475k for a seven-hour hold, where the Resolution's claim prices a 20% hold at 1% of volume per paced hour. The parameters doc prices only the rise walk and models an honest crash for the liquidation floor, so no record states an accepted bound for this route. Smallest fix: value the seizure and the bark/bite health check at the higher of the attested and paced payout price, as `cash` does.\n\n2. **Low, `src/CDPVault.sol:353`.** \"After the fee has eaten the first five\" holds only for a burn at the fee cap. A burn of 0.5% of the fee base pays 75 bps. The attached test fails on this commit: 5,000 imdUSD redeemed after one paced hour of a 20% hold returns IMD worth 5,012.63 at the pre-fall price, and 5,063.26 after two hours. What actually bounds a sustained drain is the base-rate ratchet, which caps a 22-hour drain against a $1M fee base under about $4,000. The NatSpec and record 26 should state that bound.\n\n**Answers where nothing is wrong**\n\n- **Q1.** Paid price is always at least the attested price, and the attested price is bounded below by the epoch anchor, so no fall-and-rise or rise-then-release sequence overpays; a pump underpays until it decays, 18 hours for 20% as stated. The Treasury's fall trigger buys the first attestation and the keeper's hourly pace and resecure supply the \"paced hours\", so the protocol does pay for the hold's clock. 1% an hour is a reasonable rate for `cash`, but the binding bound on a hold-down is the six-hour grace, not the 22 hours (finding 1).\n- **Q2.** Every ordering I traced holds the paced debt at or under live debt after a cancellation. The record conserves principal-time, so the merge-and-age trick costs the same 12 hours times amount. The cross-position wipe-and-redraw in one transaction keeps the aggregate held debt, which is what the ceiling measures. The stated residual (a large draw held 12 hours on a floor-sized book) is the only way under the seasoned book.\n- **Q3.** No sequence found that pays above the backing at the paid price. A bundled stale-pace, relay, cash reads at most one interval unwritten and the next pacing does not double it, because the clock is the last written time.\n- **Q4 and Q6.** `verifySeeded` matches the runbook (both references, 5% bands, NHI above 0.6). `runVault` checks the salt after the seeded check, records before verify, and `verify` refuses nonzero supply, debt or ceiling. Day-one halts are feed staleness (1h price, 2h Chainlink, 24h NHI) and divergence, each recovered by a purchase. Initcode measured at 47,961 bytes.\n- **Q5.** Over-borrowing stays unprofitable below a 70% rise, which the 60% wide-allowance refresh keeps out of reach.\n\n**Q7, read in full:** CDPVault, ParameterizedVault, SwarmFeed, Treasury, Parameters, UsdPriceFeed, SharePriceFeed, SpotFeed, PriceFeed, SwarmRelay, TransientReentrancyGuard, ImdUSD, DeploymentConfig, DeployMainnet, DeployPreflight, runbook section 7, records 25 and 26. Not read in full: OracleAsker, SwarmWorkOracle, NhiFeed, Governed, Registry, plan.py and th","treeHash":null,"usage":{"cachedInputTokens":4609979,"inputTokens":708,"model":"claude-fable-5-1","outputTokens":55126,"runtime":"claude","turns":44,"wallClockMs":990346}}],"verification":[]}