{"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":"464d2b15-5c49-45a4-8834-3c62c29e3e23","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"e0e3d0a3c4a3950b29e2c8883a86c0616700becff330c9883478f9f01c3d6e88","dependsOn":["build_contract_project"],"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":"9aae0ba85080bbb6e2b8f6a8f4757de12e9a4ddbc8b8925b2daf3cd4af011b73","dependsOn":["build_contract_project"],"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":"04fda0f3980bca86ec1fb47246eaa64402a4ecf5ed999e3f9e18ac1746ce9f2a","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"ae81ceffbaf22d9d5ec325be07e27fd9e899a4cc0b322ee61e06c9552befb74c","dependsOn":["build_contract_project"],"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":"59fe3089536337805dd78bbfcbaef4b53e1d984cb74c8abb67b6c11856daf283","dependsOn":["build_contract_project"],"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"},{"acceptedSubmissionHash":"859d72692eec08f9913f88c9a85f730008c30531b947a3731409444751a95ed3","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b6503de65ad02f845827887c23da4c3b56ccc5df7a459db263bae6e549d92f7f","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"b6503de65ad02f845827887c23da4c3b56ccc5df7a459db263bae6e549d92f7f","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"7cd29902101eb209fb89c69a9dfc06705560795999f59b6fbd85d05c161f4e63","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"7f8db58646b1b068377bc70993767061dea354f2e52da4526f1ee9a0520aebb3","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build a production-ready DeFi lending protocol and a complete user-facing web application on Ethereum Mainnet (Chain ID 1), inspired by Aave V3, where IMD (`0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7`) is used as collateral to borrow USDC, USDT, and WETH with variable-rate debt. Before implementation, research and verify Aave V3 architecture/licensing, canonical Ethereum asset addresses, IMD contract behavior and admin risks, liquidity, volatility, holder concentration, and reliable oracle options, and produce a reusable research report with source citations, evidence for key conclusions, and explicit notes where data is uncertain, incomplete, or based on assumptions. Implement the complete lending lifecycle—supply IMD, collateral management, borrowing, repayment, withdrawal, interest accrual, health factor, liquidation, reserve/risk controls, and emergency controls—using secure indexed accounting, conservative evidence-based risk parameters, robust oracle protections, and separated governance roles; prefer Multisig + Timelock for production administration. Treat the system as real-money infrastructure and validate it with Foundry unit/integration tests, fuzzing, invariants, Ethereum Mainnet fork tests, static analysis, economic attack simulations, and red-team review covering oracle manipulation, flash loans, bad debt, rounding, non-standard ERC20 behavior, liquidations, access control, upgrades, liquidity collapse, stablecoin depegs, extreme price moves, and gas stress. Build an actual production-ready frontend—not a mockup—using preferably Next.js, React, TypeScript, wagmi, viem, and WalletConnect, with support for major Ethereum wallets and working screens for Dashboard, Markets, Supply IMD, Borrow USDC/USDT/WETH, Repay, Withdraw, My Position, Liquidations, Analytics, Risk Information, and verified Contract Addresses; users must be able to connect a wallet and execute real protocol transactions through the website, while viewing balances, debt, health factor, borrowing power, rates, liquidation metrics, projected health factor, transaction status, and readable errors, with blockchain state as the source of truth. Deploy and host this frontend as a usable public website and provide the final public HTTPS URL; deployment is not complete if the frontend only runs locally. Validate the full website-to-smart-contract flow on an Ethereum Mainnet fork, including wallet connection, IMD approval and supply, collateral enablement, borrowing, repayment, withdrawal, unsafe-action rejection, oracle-driven health-factor changes, liquidation, and final state verification. Organize the swarm across research, risk/oracle, protocol architecture, Solidity, testing, economic security, frontend/UI/Web3, QA, deployment/hosting, integration, and independent audit. Execute Research → Architecture/Risk → Contracts → Frontend → Testing → Fork/Economic Attacks → Red Team → Fixes → Public Website Deployment → Deployment Rehearsal → Final Audit, and consider the project complete only when the cited research report is delivered, contracts and frontend are fully integrated, the frontend is publicly accessible at a working URL, all required tests pass, Mainnet fork E2E succeeds, Critical and High findings are zero, and final security/deployment documentation is complete. Priority: **user funds safety > solvency > accounting correctness > oracle security > liquidation reliability > frontend safety > public usability > UX > speed.**\n\nNumbers the contracts enforce: 0% fee\nToken name: IMDBANK\nToken symbol: IMDBANK","parentJobId":null,"planHash":"5e2b79e0f657528756c023dc3abdc6bab78278e973fe3c18322414776d2a590a","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"464d2b15-5c49-45a4-8834-3c62c29e3e23","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-813-imdbank"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51169","feedbackHash":"2c25787d4f3718040d87ff05edf0f0fdddde01aa03b180b564b20bd43322c536","nodeKey":"audit_economics","submissionHash":"e0e3d0a3c4a3950b29e2c8883a86c0616700becff330c9883478f9f01c3d6e88","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51168","feedbackHash":"96aae368c75d8ad5bc739dfc19b1553fcf4a60ee675a948de6ecc32e382e1a27","nodeKey":"audit_flow","submissionHash":"9aae0ba85080bbb6e2b8f6a8f4757de12e9a4ddbc8b8925b2daf3cd4af011b73","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52257","feedbackHash":"8fafd5b59840b55e6095d2de39ba0e4e301f98cef627a81301653397ed564411","nodeKey":"audit_judge","submissionHash":"4500c54fcbb5d2d47d51da4470eb512c08ca733ea0ccede2f1761e246e61abc1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51508","feedbackHash":"e1f67b3a8c0621fd2c78d77f75959b543b9a413e679d8df24fac532f1552a072","nodeKey":"audit_judge","submissionHash":"04fda0f3980bca86ec1fb47246eaa64402a4ecf5ed999e3f9e18ac1746ce9f2a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50973","feedbackHash":"18c892e890512d0514d36b3719b47b4ca49955033f7b7b19b9106c58e5cc9e77","nodeKey":"audit_math","submissionHash":"ae81ceffbaf22d9d5ec325be07e27fd9e899a4cc0b322ee61e06c9552befb74c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50903","feedbackHash":"812ea505d0f86ea216733b836e9ee1c2d26060b700c592b0fc8a8327b4d2a4a9","nodeKey":"audit_permissions","submissionHash":"59fe3089536337805dd78bbfcbaef4b53e1d984cb74c8abb67b6c11856daf283","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51500","feedbackHash":"4f6250859d32321301c215a1ee48c874c6dd9077908f3feb5b0442b6bcab1231","nodeKey":"build_contract_project","submissionHash":"aaeadfb60c0255006f3fa8e8c0e4cee9feb92d7aa0f9f0783c216e8232dc3858","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52267","feedbackHash":"76ccd13ad2d6c0a9b65f10e7cd4027c27edc04f146acac849173fbe74514214a","nodeKey":"build_contract_project","submissionHash":"859d72692eec08f9913f88c9a85f730008c30531b947a3731409444751a95ed3","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51732","feedbackHash":"2bc66792efe730d7bd5258e0885e906a342eb1ab607c6c8cda650e8ee1481bbb","nodeKey":"manifest","submissionHash":"5ef5b3838f0f7f9d184d038dce724fabe20e06d28b237484ead0bf5879f3ed30","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51259","feedbackHash":"7423c3ade612911d8dede5ba75dc645329a05efa5d4f1539e12a07c60e861bb0","nodeKey":"manifest","submissionHash":"7cd29902101eb209fb89c69a9dfc06705560795999f59b6fbd85d05c161f4e63","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51059","feedbackHash":"519e6e233f4e68a35a662c79b94bffe01f811c82481d9367e89770e8dfbe49f4","nodeKey":"write_foundry_tests","submissionHash":"49a36f82c701b679436131675cfb5e0922bb5793a63237dd8c2918033ef9ec7d","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51395","feedbackHash":"36a688caa586c6ba7e1c314387a2dd2a83bf6236130e5fd069861cb213a90f70","nodeKey":"write_foundry_tests","submissionHash":"7f8db58646b1b068377bc70993767061dea354f2e52da4526f1ee9a0520aebb3","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"e8737abef617f708471bcd67fbc996d9722645d940291b25804420baa8416c2c","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3516474d8a268bd8","findings":[{"citation":"resolved","description":"Round-2 settlement of prior finding 43d27997. The author reproduced it and chose not to change the cap semantics (an exemption for indebted accounts would let any account with $1 of debt bypass the custody cap); ARCHITECTURE.md and DEPLOYMENT.md now state that cap headroom is fillable by a debt-free depositor and that governance must size the cap with rescue headroom. The mechanism is unchanged and still reproduces: supply() applies one shared cap to top-ups by accounts that already carry debt, while a debt-free depositor can hold the headroom at no cost and withdraw at any time (debt-free withdraw skips oracle, freeze and health checks). Severity stays low (needs capital equal to the headroom, mirrors Aave cap semantics, borrower keeps repayment as a defence). Recorded for the judge as an accepted, documented residual; no change is requested for admission.","line":184,"path":"src/IMDBank.sol","reproduction":"Re-run on the revised tree in test/scratch/Round2.t.sol::test_supplyCapFillStillBlocksTopUp (passes, i.e. the block occurs): fixture defaults (cap 1,000,000e18, LTV 2500 / threshold 3500 / bonus 800), Alice supplies 1000e18 IMD and borrows 2500e6 USDC; Bob (no debt) supplies room = supplyCap - totalCollateral; IMD $10 -> $7 (Alice HF 0.98); Alice calls supply(1, ALICE). Expected: top-up accepted (it only lowers risk). Actual: revert CapExceeded(); Alice can then be liquidated at the 8% bonus and Bob withdraws the filler immediately.","severity":"low","snippet":"        if (amount > supplyCap || totalCollateral > supplyCap - amount) revert CapExceeded();","title":"Accepted residual (unchanged, documented): shared supply cap can be filled at zero cost by a debt-free depositor, blocking every borrower's collateral top-up ahead of a liquidation"},{"citation":"resolved","description":"Round-2 settlement of prior finding bcff50a7. The author reproduced it (paid 1250e6, seized 191.25e18 with 20% maxDeviationBps) and did not change the computation: the bank receives one conservative price per asset from RiskOracle (collateral low, debt high) and uses it for the health check, the covered amount and the seizure, which keeps seized <= available and solvency intact; pricing the seizure at a same-side or mid value would need a wider oracle interface. ARCHITECTURE.md now documents the realized premium formula, states the 15% bound is a parameter bound rather than a guarantee under feed disagreement, and recommends a small maxDeviationBps (5% in the examples). Still reproduces on the revised tree: RiskOracle.price (line 154) returns low for collateral-side and high for debt-side feeds and accepts disagreement up to maxDeviationBps <= 2000; _liquidationQuote lines 491-493 multiply paid by the high debt price and divide by the low collateral price. Borrower value loss under a governance-permitted configuration, no solvency impact; severity stays low. Recorded as an accepted, documented residual; no change is requested for admission.","line":493,"path":"src/IMDBank.sol","reproduction":"State: RiskOracle with IMD feeds collateralSide=true and USDC feeds collateralSide=false, maxDeviationBps 2000; IMDBank LTV 25%/threshold 35%/bonus 8%/close factor 50%. Alice supplies 1000e18 IMD and borrows 2500e6 USDC with all feeds at $10 / $1 (HF 1.4e18). Set IMD feed B = 8.4e8 (19.05% below A) and USDC feed B = 1.19e8 (19% above A): oracle.price(IMD) = 8.4e18, oracle.price(USDC) = 1.19e18, HF = 0.988e18. previewLiquidation(ALICE, USDC, max) returns paid = 1250e6 and seized = 191.25e18 (1250 x 1.19 x 1.08 / 8.4). Expected per the documented 8% bonus (15% bound): at most 1250 x 1.15 / 8.4 = 171.1e18 IMD. Actual: 191.25e18 IMD, which at feeds A is $1,912.5 received for $1,250 paid. Author confirmed in their Advisory.t.sol::test_feedSpreadPremium with identical numbers.","severity":"low","snippet":"        seized = Math.min(quote.available, Math.mulDiv(seizedValue, collateralUnit, quote.collateralPrice));","title":"Accepted residual (unchanged, documented): liquidation seizure values debt at the high feed and collateral at the low feed, so the realized liquidator premium is bonus x (1+devCollateral) x (1+devDebt"},{"citation":"resolved","description":"Round-2 settlement of prior finding aa6de509. The author confirmed the mechanism and did not add an activation ramp: GovernanceTimelock.execute (src/GovernanceTimelock.sol line 55) has no caller restriction by design (restricting it to the proposer would let a slow or lost proposer key expire every proposal), and configureRisk writes the new ltvBps/liquidationThresholdBps immediately for existing positions. ARCHITECTURE.md now states that whoever executes a scheduled cut picks its timing and may liquidate in the same transaction, and DEPLOYMENT.md instructs operators to publish scheduled changes to borrowers at scheduling time. Still reproduces on the revised tree. Severity stays low: governance-scheduled, two-day public notice, borrower can act in the window. Recorded as an accepted, documented residual; no change is requested for admission.","line":406,"path":"src/IMDBank.sol","reproduction":"Re-run on the revised tree in test/scratch/Round2.t.sol::test_paramRaceStillAtomic (passes, i.e. the atomic sequence succeeds): GovernanceTimelock(proposer=this, canceller=0x1234, 2 days) governing a fresh IMDBank with ltv 2500 / threshold 3500, IMD $10, Alice supplies 1000e18 IMD and borrows 2000e6 USDC (HF 1.75). Proposer schedules configureRisk(1000, 1500, 800, 5000, 1e24). After 2 days LIQUIDATOR calls timelock.execute(...) then bank.liquidate(ALICE, USDC, max, 0, now) in the same transaction. Expected: a parameter change alone cannot make a position liquidatable in its activation block. Actual: HF reads 0.75, liquidation repays > 2000e6 USDC and seizes > 216e18 IMD (8% bonus) with no price change.","severity":"low","snippet":"        ltvBps = ltv;","title":"Accepted residual (unchanged, documented): risk-parameter cuts apply atomically on permissionless timelock execution, so any searcher can execute and liquidate positions that were healthy one call ear"},{"citation":"resolved","description":"Round-2 settlement of prior finding c0655c7b. The author agrees it is a completeness gap and states it cannot be closed within the assignment: hosting credentials and the confirmed Mainnet contract address set from the launch handoff do not exist, and the frontend deliberately refuses invented addresses. The frontend-to-contract flow was re-validated on a local Anvil mainnet fork (web/tests/fork-integration.mjs, docs/evidence/frontend-fork.json at block 26,134,418) and the 13 frontend unit tests pass on this tree (node --test web/tests/core.test.mjs). The brief's acceptance criterion of a working public URL remains unmet; this is not a code defect and depends on later deployment-stage inputs. No change is requested of the contract code.","line":5,"path":"docs/DEPLOYMENT.md","reproduction":"web/config.json as committed: bankAddress null, oracleAddress null, bankCodeHash null, oracleCodeHash null, deploymentStatus 'NOT_DEPLOYED'. Loading web/index.html runs validateConfig in web/core.js, which throws 'Protocol deployment is not configured. Transactions are unavailable.' and every [data-write] control stays disabled. grep -n 'https://' docs/DEPLOYMENT.md returns only hosting guidance; no public URL exists in the tree. Expected per brief: a public HTTPS URL where a wallet can connect and execute protocol transactions.","severity":"info","snippet":"**Public HTTPS URL: not deployed / unavailable. Mainnet application addresses: not deployed.**","title":"Completeness gap (unchanged): the required public HTTPS frontend deployment is absent; docs/DEPLOYMENT.md states the website is not deployed and web/config.json is unpopulated"},{"citation":"resolved","description":"Round-2 settlement of prior finding 04869145. Constructor arguments are unchanged; the notes were corrected to require two authorized nonzero multisigs (a veto multisig for GovernanceTimelock.constructorArgs[1] and an emergency multisig for RiskOracle.constructorArgs[1] and IMDBank.constructorArgs[1]), distinct from each other and from $owner, and to explain why the veto and guardian roles must not be the same key. This is the right call: no address was supplied and substituting an invented one would hand control to an unauthorized party. Note for the judge: the constructors themselves cannot enforce veto != guardian (GovernanceTimelock does not know the guardian); that separation is enforced only by script/DeployMainnet.s.sol lines 28-32, the rehearsal test and the manifest notes, so whoever fills the manifest must honour it. The remaining blocker is an input gap, not a code defect.","line":8,"path":"launch.json","reproduction":"Deploy the manifest as written: new GovernanceTimelock($owner, 0x0, 172800) reverts InvalidOperation() at the canceller_ == address(0) check (src/GovernanceTimelock.sol line 25), so $contract:GovernanceTimelock never resolves; RiskOracle (line 56) and IMDBank (line 149) would also revert InvalidConfiguration() on guardian_ == address(0). Expected: a deployable manifest with two nonzero, authorized, mutually distinct role addresses. Actual: factory rehearsal fails at the first constructor.","severity":"info","snippet":"        \"0x0000000000000000000000000000000000000000\",","title":"Deployment prerequisite unmet (unchanged): launch.json passes the zero address as timelock canceller and as oracle/bank guardian, which all three constructors reject, so the manifest cannot deploy unt"}],"hash":"04fda0f3980bca86ec1fb47246eaa64402a4ecf5ed999e3f9e18ac1746ce9f2a","nodeId":"ab94eb68-e441-4a76-9d3d-3625d37e9583","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with 5 entries and a 24-row coverage record.\n\n**All four medium findings from last round are fixed.** Each attached proof now passes on the revised tree, and my own scratch test confirms the behaviour the author described:\n\n- **Oracle price band** now rejects only the direction that would overvalue a borrower. A collateral crash through the old floor and a debt spike through the ceiling are priced as reported, and a zero floor is accepted.\n- **Minimum debt and loss halt.** A borrow must leave at least $1 of debt in that reserve. Dust write-offs are recorded without halting, and lending halts only once a reserve's recorded loss exceeds $1. Unfreeze is gated on that halt flag, which clears only at full cover, so a dust finalization can no longer make a scheduled unfreeze revert.\n- **Guardian pause is bounded.** The oracle guardian gets one time-boxed pause per governance decision, governance can rotate the oracle guardian, and the deploy helper now requires three distinct multisigs.\n- **Feed age bound raised to 2 days**, so heartbeat plus grace can be configured.\n\nThe low seized-to-zero sweep gap and the frontend capacity projection are also fixed. The full project suite passes with 101 Foundry tests and 13 frontend tests.\n\n**What remains in the file is unchanged, accepted residual risk**, kept at low or info so the record is complete rather than to reopen work:\n\n- Supply cap fillable by a debt-free depositor, feed-spread liquidation premium, and atomic parameter-cut execution. The author reproduced each, chose to keep the design, and documented it. I re-ran the first and third on the revised code and they still reproduce.\n- No public HTTPS deployment, and launch.json still carries zero addresses for the veto and guardian roles. Both are input gaps that need the network owner to supply hosting and two authorized multisigs. One note for the judge: the contracts cannot enforce that veto and guardian differ, so whoever fills the manifest must honour the separation that the deploy helper and notes require.\n\nI found no new reproducible defect in the changed lines. Nothing outside `test/scratch/` and the findings file was modified.","treeHash":null,"usage":{"cachedInputTokens":1355931,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":28937,"runtime":"claude","turns":29,"wallClockMs":421273}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"22c9b760d0a15149","findings":[{"citation":"resolved","description":"RiskOracle.configure forces minPrice > 0 and maxPrice > minPrice for every feed (line 68), and price() reverts InvalidPrice whenever the lower of two fresh, agreeing feeds is below minPrice or the higher is above maxPrice, regardless of collateralSide. IMDBank._price is used by accountData, _liquidationQuote and finalizeDust, so a genuine IMD crash through the floor turns liquidate, finalizeDust, borrow and debt-bearing withdraw into reverts for every account; symmetrically a WETH price above the debt feed's maxPrice blocks liquidation of WETH borrowers. For a collateral feed a lower price can only make the borrower look worse, so the floor adds no solvency protection; it only removes liquidation liveness while interest keeps accruing and bad debt grows. The guardian cannot help (setEnabled(false) only disables the feed; configure is governor-only), the governor is a >= 2 day timelock, and RiskOracle.configure re-validates price() at execution so a reconfiguration also fails if the market moved outside the new band by then. ARCHITECTURE.md tells governance to set 'much narrower asset-specific bands' than the bank's 1e27 bound, so a production floor will sit inside the insolvency region. Merged from the economics and math specialists (same root cause). Design-preserving fix: treat only the direction that would overvalue the borrower as a hard reject (collateral side: high > maxPrice; debt side: low < minPrice) and allow minPrice == 0 to mean 'no floor', or allow the guardian to lower a collateral floor without the timelock.","line":108,"path":"src/RiskOracle.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDBank} from \"src/IMDBank.sol\";\nimport {RiskOracle} from \"src/RiskOracle.sol\";\n\ncontract BandToken {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address t, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[t] += a;\n        return true;\n    }\n}\n\ncontract BandFeed {\n    uint8 public constant decimals = 8;\n    int256 public answer;\n\n    constructor(int256 a) {\n        answer = a;\n    }\n\n    function set(int256 a) external {\n        answer = a;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, answer, block.timestamp, block.timestamp, 1);\n    }\n}\n\n/// @notice A valid, two-feed-agreed collateral price below the configured minPrice makes every\n/// price-dependent path revert, so an underwater borrower cannot be liquidated or finalized.\ncontract OracleBandBlocksLiquidationTest is Test {\n    address constant GUARDIAN = address(0xBEEF);\n    address constant ALICE = address(0xA11CE);\n    address constant LIQ = address(0xCAFE);\n\n    IMDBank bank;\n    RiskOracle oracle;\n    BandToken imd;\n    BandToken usdc;\n    BandToken usdt;\n    BandToken weth;\n    BandFeed imdA;\n    BandFeed imdB;\n\n    function setUp() public {\n        vm.warp(100 days);\n        imd = new BandToken(18);\n        usdc = new BandToken(6);\n        usdt = new BandToken(6);\n        weth = new BandToken(18);\n        oracle = new RiskOracle(address(this), GUARDIAN);\n        imdA = new BandFeed(10e8);\n        imdB = new BandFeed(10e8);\n        // Collateral band $5..$50 around the $10 reference price; stablecoin and WETH bands are generous.\n        oracle.configure(address(imd), address(imdA), address(imdB), 1 hours, 1 hours, 500, 5e18, 50e18, true);\n        oracle.configure(\n            address(usdc), address(new BandFeed(1e8)), address(new BandFeed(1e8)), 1 days, 1 days, 500, 0.9e18, 1.1e18, false\n        );\n        oracle.configure(\n            address(usdt), address(new BandFeed(1e8)), address(new BandFeed(1e8)), 1 days, 1 days, 500, 0.9e18, 1.1e18, false\n        );\n        oracle.configure(\n            address(weth),\n            address(new BandFeed(2000e8)),\n            address(new BandFeed(2000e8)),\n            1 hours,\n            1 hours,\n            500,\n            100e18,\n            100_000e18,\n            false\n        );\n        bank = new IMDBank(\n            address(this), GUARDIAN, address(imd), address(oracle), address(usdc), address(usdt), address(weth)\n        );\n        bank.configureRisk(2500, 3500, 800, 5000, 1e30);\n        bank.configureReserve(address(usdc), 1e30, 0.02e27, 0.08e27, 0.9e27, 8000);\n        bank.setReserveFrozen(address(usdc), false);\n        bank.setFrozen(false);\n        usdc.mint(address(this), 1_000_000e6);\n        usdc.approve(address(bank), type(uint256).max);\n        bank.donateLiquidity(address(usdc), 1_000_000e6);\n\n        imd.mint(ALICE, 1000e18);\n        vm.startPrank(ALICE);\n        imd.approve(address(bank), type(uint256).max);\n        bank.supply(1000e18, ALICE);\n        bank.setCollateralEnabled(true);\n        bank.borrow(address(usdc), 2500e6, ALICE); // $2,500 against $10,000 of IMD (max LTV)\n        vm.stopPrank();\n\n        usdc.mint(LIQ, 1_000_000e6);\n        vm.prank(LIQ);\n        usdc.approve(address(bank), type(uint256).max);\n    }\n\n    function test_collateralCrashBelowFloorMustStillBeLiquidatable() public {\n        // IMD crashes 60% to $4.00. Both independent feeds agree exactly; nothing is stale or broken.\n        imdA.set(4e8);\n        imdB.set(4e8);\n        // Collateral is now $4,000 against $2,500 debt: HF = 4000 * 0.35 / 2500 = 0.56.\n        // The position is deeply liquidatable and still fully recoverable (collateral > 1.08 * debt).\n        vm.prank(LIQ);\n        (uint256 repaid, uint256 seized) =\n            bank.liquidate(ALICE, address(usdc), type(uint256).max, 0, block.timestamp);\n        assertGt(repaid, 0, \"underwater borrower was not liquidatable\");\n        assertGt(seized, 0, \"no collateral seized\");\n        assertEq(bank.previewDebt(ALICE, address(usdc)), 0, \"full close should have been possible\");\n    }\n}","reproduction":"State: RiskOracle with IMD feeds A and B at 10e8 (8 decimals), configured minPrice 5e18, maxPrice 50e18, maxDeviation 500 bps, collateralSide true; USDC/USDT/WETH feeds in band; IMDBank LTV 2500 / threshold 3500 / bonus 800; Alice supplies 1000e18 IMD and borrows 2500e6 USDC. Both IMD feeds move to 4e8 ($4.00, exact agreement, fresh). Call bank.liquidate(ALICE, USDC, type(uint256).max, 0, block.timestamp) from a funded liquidator. Expected: HF = 4000*0.35/2500 = 0.56 and collateral (4000) > 1.08 * 2500, so the call repays 2500e6 and seizes 675e18 IMD. Actual: reverts InvalidPrice() from RiskOracle.price because low (4e18) < minPrice (5e18); bank.accountData(ALICE) and bank.finalizeDust(ALICE) revert the same way. At exactly 5e8 the same calls succeed with HF 0.70. Also: configure(..., minPrice = 0, ...) reverts InvalidConfiguration, so 'no floor' cannot be expressed. Proof test/scratch/Proof_936e1e193033.t.sol fails on the current tree with InvalidPrice().","severity":"medium","snippet":"            low < f.minPrice || high > f.maxPrice","title":"Hard oracle price band rejects a valid two-feed-agreed collateral crash (or debt spike), so liquidation, finalizeDust, borrow and debt-bearing withdraw all revert exactly when loss recognition is need"},{"citation":"resolved","description":"borrow() admits any amount >= 1 base unit; shares are ceil(amount*RAY/index) and displayed debt is ceil(shares*index/RAY). With one share at index RAY, the first accrual (index = 1e27 + ceil(0.02e27/365 days)) makes the displayed debt 2 units: a 100% jump with no price move, dropping HF from 1.40 to 0.70. For such a position _liquidationQuote needs coveredAmount >= 1 and burned = floor(budget*RAY/index) >= 1, while finalizeDust needs ceil(collateralUsd) < debtUsd and no reserve able to burn a share; when collateral sits between 1.00x and 1.08x of the 1-2 unit debt neither path can execute (liquidate reverts Dust, finalizeDust reverts InvalidAmount). Once collateral falls below the debt (a 51% IMD move from the 25% LTV entry), finalizeDust succeeds, _writeOff records 2 units of bad debt and sets the global frozen flag. While frozen: borrow reverts for everyone and withdraw reverts Frozen for every account that has any debt (line 195), so healthy borrowers cannot reduce exposure except by full repayment; setFrozen(false) reverts OutstandingBadDebt until coverBadDebt. Because finalizeDust is permissionless, a griefer with several pre-positioned micro-accounts (cost ~4e11 wei IMD + 1 USDC unit each) can finalize another one just before a matured timelock.execute(setFrozen(false)) so the execute reverts ExecutionFailed(OutstandingBadDebt()), and can repeat after each restart while the price stays low. The author records the global halt as the accepted M-05 residual; this finding sharpens it (ceil rounding halves the required move, the intermediate band is unreachable by both paths, and debt-bearing withdrawals are also blocked). Merged from the math and permissions specialists. Design-preserving fix: enforce a minimum debt value per account/asset in borrow() (e.g. debtUsd >= 1e18 after the mint) so one-share debts cannot exist, and/or record sub-threshold dust losses against the affected reserve only instead of flipping the global frozen flag.","line":510,"path":"src/IMDBank.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDBank} from \"src/IMDBank.sol\";\n\ncontract DustToken {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address t, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[t] += a;\n        return true;\n    }\n}\n\ncontract DustOracle {\n    mapping(address => uint256) public p;\n\n    function set(address a, uint256 v) external {\n        p[a] = v;\n    }\n\n    function price(address a) external view returns (uint256) {\n        require(p[a] != 0, \"no price\");\n        return p[a];\n    }\n}\n\n/// @notice A one-base-unit USDC debt ($0.000001) is admitted, its displayed debt doubles to two units after\n/// one second of accrual through ceil rounding, it is then neither liquidatable nor finalizable while the\n/// collateral still covers it, and after a 51% IMD move it is finalized as bad debt and freezes the entire bank.\ncontract DustPositionGlobalFreezeTest is Test {\n    address constant GUARDIAN = address(0xBEEF);\n    address constant ATTACKER = address(0xA77AC);\n    address constant LIQ = address(0xCAFE);\n\n    IMDBank bank;\n    DustOracle oracle;\n    DustToken imd;\n    DustToken usdc;\n    DustToken usdt;\n    DustToken weth;\n\n    function setUp() public {\n        vm.warp(100 days);\n        imd = new DustToken(18);\n        usdc = new DustToken(6);\n        usdt = new DustToken(6);\n        weth = new DustToken(18);\n        oracle = new DustOracle();\n        oracle.set(address(imd), 10e18);\n        oracle.set(address(usdc), 1e18);\n        oracle.set(address(usdt), 1e18);\n        oracle.set(address(weth), 2000e18);\n        bank = new IMDBank(\n            address(this), GUARDIAN, address(imd), address(oracle), address(usdc), address(usdt), address(weth)\n        );\n        bank.configureRisk(2500, 3500, 800, 5000, 1e30);\n        bank.configureReserve(address(usdc), 1e30, 0.02e27, 0.08e27, 0.9e27, 8000);\n        bank.setReserveFrozen(address(usdc), false);\n        bank.setFrozen(false);\n        usdc.mint(address(this), 1_000_000e6);\n        usdc.approve(address(bank), type(uint256).max);\n        bank.donateLiquidity(address(usdc), 1_000_000e6);\n        imd.mint(ATTACKER, 1e18);\n        usdc.mint(LIQ, 1e12);\n        vm.prank(ATTACKER);\n        imd.approve(address(bank), type(uint256).max);\n        vm.prank(LIQ);\n        usdc.approve(address(bank), type(uint256).max);\n    }\n\n    function test_oneUnitDebtCannotFreezeTheWholeBank() public {\n        vm.startPrank(ATTACKER);\n        bank.supply(4e11, ATTACKER); // 0.0000004 IMD = $0.000004 at $10\n        bank.setCollateralEnabled(true);\n        // Exactly at the 25% LTV: one USDC base unit ($0.000001). A minimum-debt guard would reject this.\n        try bank.borrow(address(usdc), 1, ATTACKER) {}\n        catch {\n            vm.stopPrank();\n            return;\n        }\n        vm.stopPrank();\n        assertEq(bank.previewDebt(ATTACKER, address(usdc)), 1);\n\n        // One second later the one-share debt is ceil(1 * (1e27 + ceil(0.02e27/365d)) / 1e27) = 2 units.\n        vm.warp(block.timestamp + 1);\n        bank.accrue(address(usdc));\n        assertEq(bank.previewDebt(ATTACKER, address(usdc)), 2, \"ceil rounding doubled a one-share debt\");\n        (,,,, uint256 hf) = bank.accountData(ATTACKER);\n        assertLt(hf, 1e18); // 4e12 * 0.35 / 2e12 = 0.70 without any price move\n\n        // A 51% move ($10 -> $4.90). Collateral $0.00000196 < debt $0.000002.\n        oracle.set(address(imd), 4.9e18);\n        // Ordinary liquidation is impossible: covered amount floors to 1 unit, which cannot burn one share.\n        vm.prank(LIQ);\n        vm.expectRevert(IMDBank.Dust.selector);\n        bank.liquidate(ATTACKER, address(usdc), type(uint256).max, 0, block.timestamp);\n\n        // Anyone can now write the position off. Two base units of loss must not halt all lending.\n        try bank.finalizeDust(ATTACKER) {} catch {}\n        assertFalse(bank.frozen(), \"a $0.000002 loss froze the entire bank\");\n    }\n}","reproduction":"State: fixture defaults (LTV 2500, threshold 3500, bonus 800, USDC reserve base rate 2%, IMD $10, bank unfrozen, USDC reserve funded). Attacker: supply(4e11, attacker); setCollateralEnabled(true); borrow(USDC, 1, attacker) succeeds (capacity 1e12 USD-wei = exactly 1 unit). previewDebt == 1. vm.warp(+1 s): previewDebt == 2 (expected 1 plus 6.3e-10 interest); accountData HF == 0.70e18. Set IMD to $5.20 (collateral 2.08e12 USD-wei vs debt 2e12): liquidate(attacker, USDC, max, 0, now) reverts Dust() and finalizeDust(attacker) reverts InvalidAmount() -- unreachable band. Set IMD to $4.90 (collateral 1.96e12 < debt 2e12): liquidate still reverts Dust(); finalizeDust(attacker) succeeds, reserveData(USDC).badDebt == 2, bank.frozen() == true, and setFrozen(false) by the governor reverts OutstandingBadDebt(). Expected: a $0.000002 position cannot stop lending and debt-bearing withdrawals for every user. Reproduced in test/scratch/Repro.t.sol::test_oneUnitDebtDoublesAndStuckBand (passes, i.e. the reverts and the freeze occur). Proof test/scratch/Proof_4ebffbf6febd.t.sol fails on the current tree with 'a $0.000002 loss froze the entire bank'.","severity":"medium","snippet":"            frozen = true;","title":"No minimum position size: a one-base-unit debt doubles after one second of accrual, is unreachable by both liquidate and finalizeDust in a price band, and then a $0.000002 loss freezes the whole bank "},{"citation":"resolved","description":"RiskOracle.setEnabled(asset, false) is an instant guardian power and RiskOracle.guardian has no rotation function (IMDBank.setGuardian only rotates the bank guardian). While the IMD feed is disabled, IMDBank._price reverts Disabled() for liquidate, finalizeDust, borrow and every debt-bearing withdraw, so unhealthy positions cannot be liquidated while collateral falls and the resulting bad debt cannot even be recognized. script/DeployMainnet.s.sol lines 35-37 wire the same emergencyMultisig as GovernanceTimelock.canceller, RiskOracle.guardian and IMDBank.guardian, and GovernanceTimelock.cancel (line 49) lets the canceller cancel any pending operation. Every reversal (setEnabled(IMD, true), setFrozen(false), setGuardian) must pass through the >= 2 day window during which that key cancels it; even if one slipped through, the oracle guardian re-disables in the next block forever. The docs state the opposite ('the canceller can veto, not execute or replace'; guardian actions are reversible by governance), and GovernanceTimelock's own NatSpec says proposer and canceller should be distinct reviewed multisigs. Severity medium because it needs a compromised or rogue privileged key, but the trust gap is material and undocumented: an emergency pause becomes a permanent state and reserve donors absorb unbounded, unrecognizable bad debt. Design-preserving fix: give governance a timelocked RiskOracle.setGuardian mirroring IMDBank.setGuardian, and deploy the timelock canceller as an address distinct from the bank/oracle guardian (enforce it in DeployMainnet and the manifest notes).","line":32,"path":"src/RiskOracle.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDBank} from \"src/IMDBank.sol\";\nimport {RiskOracle} from \"src/RiskOracle.sol\";\nimport {GovernanceTimelock} from \"src/GovernanceTimelock.sol\";\n\ncontract VetoToken {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract VetoFeed {\n    uint8 public decimals = 8;\n    int256 public answer;\n\n    constructor(int256 a) {\n        answer = a;\n    }\n\n    function set(int256 a) external {\n        answer = a;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, answer, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract Multisig {}\n\n/// @notice Production wiring: timelock(proposer=governance, canceller=emergency), oracle(timelock, emergency),\n/// bank(timelock, emergency). The emergency key can hold every price-dependent path closed forever.\ncontract GuardianVetoTest is Test {\n    VetoToken imd;\n    VetoToken usdc;\n    VetoToken usdt;\n    VetoToken weth;\n    VetoFeed imdA;\n    VetoFeed imdB;\n    GovernanceTimelock timelock;\n    RiskOracle oracle;\n    IMDBank bank;\n    address governance;\n    address emergency;\n    address constant ALICE = address(0xA11CE);\n    address constant LIQUIDATOR = address(0xCAFE);\n\n    function setUp() public {\n        vm.warp(100 days);\n        governance = address(new Multisig());\n        emergency = address(new Multisig());\n        imd = new VetoToken(18);\n        usdc = new VetoToken(6);\n        usdt = new VetoToken(6);\n        weth = new VetoToken(18);\n        timelock = new GovernanceTimelock(governance, emergency, 2 days);\n        oracle = new RiskOracle(address(timelock), emergency);\n        bank = new IMDBank(address(timelock), emergency, address(imd), address(oracle), address(usdc), address(usdt), address(weth));\n\n        imdA = new VetoFeed(10e8);\n        imdB = new VetoFeed(10e8);\n        VetoFeed usdA = new VetoFeed(1e8);\n        VetoFeed usdB = new VetoFeed(1e8);\n        VetoFeed ethA = new VetoFeed(2000e8);\n        VetoFeed ethB = new VetoFeed(2000e8);\n        _govern(address(oracle), abi.encodeCall(oracle.configure, (address(imd), address(imdA), address(imdB), 1 days, 1 days, 500, 1e15, 1e24, true)), 0);\n        _govern(address(oracle), abi.encodeCall(oracle.configure, (address(usdc), address(usdA), address(usdB), 1 days, 1 days, 500, 1e15, 1e24, false)), 1);\n        _govern(address(oracle), abi.encodeCall(oracle.configure, (address(usdt), address(usdA), address(usdB), 1 days, 1 days, 500, 1e15, 1e24, false)), 2);\n        _govern(address(oracle), abi.encodeCall(oracle.configure, (address(weth), address(ethA), address(ethB), 1 days, 1 days, 500, 1e15, 1e24, false)), 3);\n        _govern(address(bank), abi.encodeCall(bank.configureRisk, (2500, 3500, 800, 5000, 1_000_000e18)), 4);\n        _govern(address(bank), abi.encodeCall(bank.configureReserve, (address(usdc), 1_000_000e6, 0.02e27, 0.08e27, 0.9e27, 8000)), 5);\n        _govern(address(bank), abi.encodeCall(bank.setFrozen, (false)), 6);\n\n        usdc.mint(address(this), 1_000_000e6);\n        usdc.approve(address(bank), type(uint256).max);\n        bank.donateLiquidity(address(usdc), 1_000_000e6);\n        imd.mint(ALICE, 1000e18);\n        vm.startPrank(ALICE);\n        imd.approve(address(bank), type(uint256).max);\n        bank.supply(1000e18, ALICE);\n        bank.setCollateralEnabled(true);\n        bank.borrow(address(usdc), 2000e6, ALICE);\n        vm.stopPrank();\n        usdc.mint(LIQUIDATOR, 1_000_000e6);\n        vm.prank(LIQUIDATOR);\n        usdc.approve(address(bank), type(uint256).max);\n    }\n\n    function _govern(address target, bytes memory data, uint256 salt) internal {\n        vm.prank(governance);\n        timelock.schedule(target, data, bytes32(salt));\n        vm.warp(block.timestamp + 2 days);\n        timelock.execute(target, data, bytes32(salt));\n    }\n\n    /// @dev Fails on the current tree: the emergency key is both guardian and canceller, so every governance\n    /// operation that would reverse the guardian's action (re-enable the feed, unfreeze, rotate the guardian)\n    /// is vetoed by the same key, and the oracle guardian is immutable anyway. Liquidation stays impossible.\n    function test_emergencyKeyCannotPermanentlyBlockLiquidations() public {\n        // 1. Emergency key disables the collateral feed and freezes the bank (allowed, instant).\n        vm.startPrank(emergency);\n        oracle.setEnabled(address(imd), false);\n        bank.setFrozen(true);\n        vm.stopPrank();\n\n        // 2. IMD crashes 60%; Alice's position is deeply unhealthy but no one can liquidate: Disabled().\n        imdA.set(4e8);\n        imdB.set(4e8);\n        vm.prank(LIQUIDATOR);\n        vm.expectRevert(RiskOracle.Disabled.selector);\n        bank.liquidate(ALICE, address(usdc), type(uint256).max, 0, block.timestamp);\n\n        // 3. Governance tries every reversal; the emergency key cancels each one before it matures.\n        bytes memory reenable = abi.encodeCall(oracle.setEnabled, (address(imd), true));\n        bytes memory unfreeze = abi.encodeCall(bank.setFrozen, (false));\n        bytes memory rotate = abi.encodeCall(bank.setGuardian, (address(0xD00D)));\n        vm.startPrank(governance);\n        bytes32 id1 = timelock.schedule(address(oracle), reenable, bytes32(uint256(100)));\n        bytes32 id2 = timelock.schedule(address(bank), unfreeze, bytes32(uint256(101)));\n        bytes32 id3 = timelock.schedule(address(bank), rotate, bytes32(uint256(102)));\n        vm.stopPrank();\n        vm.startPrank(emergency);\n        timelock.cancel(id1);\n        timelock.cancel(id2);\n        timelock.cancel(id3);\n        vm.stopPrank();\n        vm.warp(block.timestamp + 2 days);\n        vm.expectRevert(GovernanceTimelock.NotReady.selector);\n        timelock.execute(address(oracle), reenable, bytes32(uint256(100)));\n        vm.expectRevert(GovernanceTimelock.NotReady.selector);\n        timelock.execute(address(bank), unfreeze, bytes32(uint256(101)));\n        vm.expectRevert(GovernanceTimelock.NotReady.selector);\n        timelock.execute(address(bank), rotate, bytes32(uint256(102)));\n\n        // 4. Even if a reversal slipped through, the oracle guardian is immutable and re-disables instantly.\n        assertEq(oracle.guardian(), emergency);\n\n        // Expected: some governance path restores liquidation. Actual: none exists.\n        (,,,, uint256 hf) = _safeAccountData(ALICE);\n        bool liquidatable;\n        vm.prank(LIQUIDATOR);\n        try bank.liquidate(ALICE, address(usdc), type(uint256).max, 0, block.timestamp) {\n            liquidatable = true;\n        } catch {}\n        assertTrue(liquidatable, \"liquidation permanently blocked by emergency key\");\n        hf;\n    }\n\n    function _safeAccountData(address account) internal view returns (uint256, uint256, uint256, uint256, uint256) {\n        try bank.accountData(account) returns (uint256 a, uint256 b, uint256 c, uint256 d, uint256 e) {\n            return (a, b, c, d, e);\n        } catch {\n            return (0, 0, 0, 0, 0);\n        }\n    }\n}","reproduction":"State: DeployMainnet wiring (timelock(proposer=G, canceller=E, 2 days), oracle(timelock, E), bank(timelock, E)); IMD feeds at $10, Alice supplies 1000 IMD and borrows 2000 USDC. Steps: (1) E calls oracle.setEnabled(IMD, false) and bank.setFrozen(true). (2) Both IMD feeds fall to $4 (HF ~0.7). (3) LIQUIDATOR calls bank.liquidate(Alice, USDC, max, 0, now): reverts RiskOracle.Disabled(). (4) G schedules oracle.setEnabled(IMD, true), bank.setFrozen(false) and bank.setGuardian(0xD00D); E calls timelock.cancel on all three ids; after 2 days each execute reverts NotReady. (5) oracle.guardian() is still E and no function can change it. Expected: governance can always restore liquidation after an emergency action. Actual: liquidation and bad-debt recognition stay blocked for as long as E wishes. Proof test/scratch/Proof_f15da0369580.t.sol fails on the current tree with 'liquidation permanently blocked by emergency key'.","severity":"medium","snippet":"    address public immutable guardian;","title":"RiskOracle guardian is immutable and the production wiring makes the same emergency key the timelock canceller, so one key can keep liquidations and loss recognition blocked indefinitely with no gover"},{"citation":"resolved","description":"RiskOracle.configure rejects any primaryMaxAge/secondaryMaxAge above 86,400 s and _read (line 120) reverts InvalidPrice when block.timestamp - updated > maxAge. The project's own evidence (docs/evidence/chainlink-mainnet-catalog.json) records the canonical Chainlink USDT/USD feed with heartbeat 86,400 s and USDC/USD with 82,800 s, both 0.25% deviation. In calm markets such a feed only refreshes at the heartbeat, and the heartbeat round is mined some seconds to minutes after the heartbeat elapses, so the latest round is routinely older than 86,400 s for part of every day. During that window price(USDT) reverts; because accountData prices every reserve an account owes, every account with USDT (or, with one hour of slack, USDC) debt cannot be liquidated in any asset, cannot be dust-finalized, cannot borrow and cannot make a debt-bearing withdrawal, while interest accrues. Standard Chainlink integration guidance is heartbeat plus a buffer, which the contract cannot express; DEPLOYMENT.md acknowledges the risk but there is no configuration that avoids it. The brief ranks liquidation reliability above UX, and a collateral crash coinciding with the quiet period is exactly the case liquidations exist for. Merged from the economics and control-flow specialists. Fix: raise the bound (e.g. allow up to 2 days, or heartbeat + grace per feed) while leaving governance responsible for per-feed values, or add a bounded grace above maxAge inside _read.","line":67,"path":"src/RiskOracle.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDBank} from \"src/IMDBank.sol\";\nimport {RiskOracle} from \"src/RiskOracle.sol\";\n\ncontract HbToken {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address t, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[t] += a;\n        return true;\n    }\n}\n\n/// @dev Feed whose round timestamp is pinned, like a Chainlink stablecoin feed between heartbeats.\ncontract HbFeed {\n    uint8 public constant decimals = 8;\n    int256 public answer;\n    uint256 public updated;\n\n    constructor(int256 a) {\n        answer = a;\n        updated = block.timestamp;\n    }\n\n    function set(int256 a) external {\n        answer = a;\n        updated = block.timestamp;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, answer, updated, updated, 1);\n    }\n}\n\n/// @notice The USDT/USD and USDC/USD Chainlink feeds the project selected have 86,400 s heartbeats, and the\n/// heartbeat round lands after the heartbeat elapses. RiskOracle caps maxAge at exactly 1 day and has no\n/// grace, so every stablecoin-debt position is unliquidatable for part of every day. Fails on the current\n/// tree; passes once governance can configure heartbeat plus a buffer (larger bound) or the oracle grants a\n/// built-in grace above maxAge.\ncontract HeartbeatWindowProofTest is Test {\n    address constant GUARDIAN = address(0xBEEF);\n    address constant ALICE = address(0xA11CE);\n    address constant LIQ = address(0xCAFE);\n\n    IMDBank bank;\n    RiskOracle oracle;\n    HbToken imd;\n    HbToken usdc;\n    HbToken usdt;\n    HbToken weth;\n    HbFeed imdA;\n    HbFeed imdB;\n    HbFeed usdtA;\n    HbFeed usdtB;\n\n    function setUp() public {\n        vm.warp(100 days);\n        imd = new HbToken(18);\n        usdc = new HbToken(6);\n        usdt = new HbToken(6);\n        weth = new HbToken(18);\n        oracle = new RiskOracle(address(this), GUARDIAN);\n        imdA = new HbFeed(10e8);\n        imdB = new HbFeed(10e8);\n        usdtA = new HbFeed(1e8);\n        usdtB = new HbFeed(1e8);\n        oracle.configure(address(imd), address(imdA), address(imdB), 1 days, 1 days, 500, 1e15, 1e24, true);\n        oracle.configure(\n            address(usdc), address(new HbFeed(1e8)), address(new HbFeed(1e8)), 1 days, 1 days, 500, 1e15, 1e24, false\n        );\n        oracle.configure(\n            address(weth), address(new HbFeed(2000e8)), address(new HbFeed(2000e8)), 1 days, 1 days, 500, 1e15, 1e24, false\n        );\n        // Operator intent: heartbeat (86,400 s) plus a one-hour buffer. Fall back to the only admissible value.\n        try oracle.configure(address(usdt), address(usdtA), address(usdtB), 1 days + 1 hours, 1 days + 1 hours, 500, 1e15, 1e24, false) {}\n        catch {\n            oracle.configure(address(usdt), address(usdtA), address(usdtB), 1 days, 1 days, 500, 1e15, 1e24, false);\n        }\n        bank = new IMDBank(\n            address(this), GUARDIAN, address(imd), address(oracle), address(usdc), address(usdt), address(weth)\n        );\n        bank.configureRisk(2500, 3500, 800, 5000, 1e30);\n        bank.configureReserve(address(usdt), 1e30, 0.02e27, 0.08e27, 0.9e27, 8000);\n        bank.setFrozen(false);\n        usdt.mint(address(this), 1_000_000e6);\n        usdt.approve(address(bank), type(uint256).max);\n        bank.donateLiquidity(address(usdt), 1_000_000e6);\n        imd.mint(ALICE, 1000e18);\n        vm.startPrank(ALICE);\n        imd.approve(address(bank), type(uint256).max);\n        bank.supply(1000e18, ALICE);\n        bank.setCollateralEnabled(true);\n        bank.borrow(address(usdt), 2500e6, ALICE);\n        vm.stopPrank();\n        usdt.mint(LIQ, 1_000_000e6);\n        vm.prank(LIQ);\n        usdt.approve(address(bank), type(uint256).max);\n    }\n\n    function test_stablecoinHeartbeatWindowMustNotBlockLiquidation() public {\n        uint256 t0 = block.timestamp;\n        // One minute after the USDT heartbeat elapsed; the heartbeat round has not landed yet.\n        vm.warp(t0 + 1 days + 60);\n        // IMD crashes 50% with fresh IMD feeds: collateral $5,000 against $2,500 debt, HF 0.70.\n        imdA.set(5e8);\n        imdB.set(5e8);\n        vm.prank(LIQ);\n        (uint256 repaid, uint256 seized) =\n            bank.liquidate(ALICE, address(usdt), type(uint256).max, 0, block.timestamp);\n        assertGt(repaid, 0, \"underwater USDT borrower not liquidatable inside the heartbeat window\");\n        assertGt(seized, 0);\n    }\n}","reproduction":"State: RiskOracle with two USDT feeds (8 decimals) configured as configure(USDT, feedA, feedB, 1 days, 1 days, 500, 1e15, 1e24, false); configure(..., 1 days + 1, ...) reverts InvalidConfiguration so no larger value is possible. IMDBank LTV 25%/threshold 35%; Alice supplies 1000 IMD at $10 and borrows 2500 USDT (HF 1.4). Both USDT feeds report updatedAt = t0. At t0 + 86,400 accountData(ALICE) succeeds. Warp to t0 + 86,460 (one minute past the heartbeat, before the heartbeat round lands) and move the fresh IMD feeds to $5 so HF would be 0.70. Expected: liquidate(ALICE, USDT, max, 0, now) repays and seizes. Actual: oracle.price(USDT), bank.accountData(ALICE) and bank.liquidate(...) all revert InvalidPrice. Reproduced in test/scratch/Repro.t.sol::test_heartbeatWindow; proof test/scratch/HeartbeatProof.t.sol fails on the current tree with InvalidPrice() and passes under either a raised bound (it first tries 1 days + 1 hours) or a built-in grace.","severity":"medium","snippet":"                || primaryMaxAge > 1 days || secondaryMaxAge > 1 days || maxDeviationBps == 0","title":"Oracle maxAge is hard-capped at exactly 1 day with no grace, equal to the 86,400 s heartbeat of the selected USDT/USD feed, so stablecoin-debt positions are unliquidatable for a window every day"},{"citation":"resolved","description":"supply() enforces one shared cap on all deposits, including top-ups by accounts that already carry debt. A debt-free depositor pays nothing, earns nothing and can withdraw at any time (debt-free withdraw skips the oracle, the freeze and the health check). So a griefer holding the remaining headroom (supplyCap - totalCollateral) during a volatile period makes every borrower's defensive supply() revert CapExceeded; the borrower's only remaining defence is repaying in the debt token. The griefer or anyone then liquidates for the 8% bonus and withdraws the filler IMD immediately, even while the bank is frozen or the oracle is down. The tighter governance sets the cap (the docs say production caps should be orders of magnitude smaller) the cheaper the attack, and the >= 2 day timelock cannot raise the cap in time. Low because it requires capital equal to the headroom and mirrors Aave's own cap semantics; reported because the brief ranks user-funds safety first and ARCHITECTURE.md's 'rescue headroom' advice is defeated by the headroom itself being fillable. Design-preserving fix: let an account that already has debt add collateral past the cap (a top-up only reduces protocol risk), or cap only collateral backing debt.","line":175,"path":"src/IMDBank.sol","reproduction":"State: fixture defaults, supplyCap 1,000,000e18, Alice 1000e18 IMD / 2500e6 USDC debt. Griefer supplies room = supplyCap - totalCollateral (999,000e18 IMD) with no debt. IMD $10 -> $7 (Alice HF 0.98). Alice calls supply(1, ALICE) holding IMD: expected success (top-up lowers risk), actual revert CapExceeded. Liquidator liquidates Alice: repaid 1,250e6 USDC, seized 192.857e18 IMD (= $1,350 at $7, an 8% premium Alice could have avoided). Griefer calls withdraw(room, griefer) and gets all 999,000e18 IMD back. Reproduced in test/scratch/Repro.t.sol::test_supplyCapFillBlocksTopUp.","severity":"low","snippet":"        if (amount > supplyCap || totalCollateral > supplyCap - amount) revert CapExceeded();","title":"Shared supply cap can be filled at zero cost by a debt-free depositor, blocking every borrower's collateral top-up ahead of a liquidation the filler can then execute and unwind"},{"citation":"resolved","description":"RiskOracle.price returns low for collateral-side feeds and high for debt-side feeds and accepts disagreement up to maxDeviationBps (configurable to 2000). Using the conservative side for the health-factor check is defensible, but _liquidationQuote uses the same two prices to compute how much collateral the liquidator receives: seizedValue = paid x debtPrice(high) x (1+bonus) (line 474) and seized = seizedValue / collateralPrice(low) (line 475). With both pairs disagreeing inside the permitted band the liquidator receives (1+bonus)(1+devIMD)(1+devUSDC) of value measured at the other, equally valid, feed values. configureRisk caps the bonus at 15% and enforces threshold*(BPS+bonus) < BPS*BPS to keep a buffer, but the effective borrower-to-liquidator transfer can be ~1.65x at 2000 bps deviation, or ~1.19x at the 500 bps used in the docs' examples. Borrower value loss under a specific but governance-permitted configuration; solvency is unaffected (seized <= available still holds). Design-preserving fix: compute the seizure with same-side prices for both legs (e.g. both low, or the mid of each accepted pair) while keeping the conservative HF rule.","line":475,"path":"src/IMDBank.sol","reproduction":"State: RiskOracle with IMD feeds collateralSide=true and USDC feeds collateralSide=false, maxDeviationBps 2000; IMDBank LTV 25%/threshold 35%/bonus 8%/close factor 50%. Alice supplies 1000e18 IMD and borrows 2500e6 USDC with all feeds at $10 / $1 (HF 1.4e18). Set IMD feed B = 8.4e8 (19.05% below A) and USDC feed B = 1.19e8 (19% above A): oracle.price(IMD) = 8.4e18, oracle.price(USDC) = 1.19e18, HF = 0.988e18. previewLiquidation(ALICE, USDC, max) returns paid = 1250e6 and seized = 191.25e18 (1250 x 1.19 x 1.08 / 8.4). Expected per the documented 8% bonus (15% bound): at most 1250 x 1.15 / 8.4 = 171.1e18, or 135e18 valued at feeds A. Actual: 191.25e18 IMD, which at feeds A is $1,912.5 received for $1,250 paid (53% premium). Reproduced in test/scratch/Repro.t.sol::test_feedSpreadPremium.","severity":"low","snippet":"        seized = Math.min(quote.available, Math.mulDiv(seizedValue, collateralUnit, quote.collateralPrice));","title":"Liquidation seizure values debt at the HIGH feed and collateral at the LOW feed, so the realized liquidator premium is bonus x (1+devCollateral) x (1+devDebt) and exceeds the 15% liquidationBonusBps b"},{"citation":"resolved","description":"_liquidationQuote computes seized = floor(seizedValue * collateralUnit / collateralPrice). When one wei of IMD is worth more than paid x debtPrice x 1.08 (collateral price near the 1e27 MAX_PRICE bound with a few wei of collateral), seized rounds to 0 while paid > 0. The dust sweep that would hand over the whole residual requires seized != 0, so the quote falls through to 'if (paid == 0 || seized == 0) revert Dust()'. finalizeDust then rejects the same account because _requireUnliquidatableDust sees paid != 0 (a share is affordable). The account is unreachable by both paths. The precondition is only reachable at the accepted price boundary, so this is a logic gap with a trivial fix rather than a live loss: drop the 'seized != 0' conjunct (the paid == 0 check after it still protects the liquidator).","line":477,"path":"src/IMDBank.sol","reproduction":"State: fixture defaults with MockOracle; set IMD = 1e27 (MAX_PRICE), WETH = 2000e18. supply(1, alice) (1 wei IMD = 1e9 USD-wei), setCollateralEnabled(true), borrow(WETH, 125000, alice) (debt 2.5e8 USD-wei, capacity 2.5e8). Set IMD to 2e26: accountData collateral 2e8, debt 2.5e8, HF 0.28e18. liquidate(alice, WETH, max, 0, now): coveredAmount = 92592, paid = 92592, seizedValue = 199998720, seized = floor(199998720 * 1e18 / 2e26) = 0, sweep skipped, reverts Dust(). finalizeDust(alice): ceil collateral 2e8 <= 1e15 and < 2.5e8, but _requireUnliquidatableDust computes paid = 92592 != 0 and reverts InvalidAmount(). Expected: with budget == coveredAmount and paid > 0 the sweep sets seized = available (1 wei) so the liquidator closes the position. Reproduced in test/scratch/Repro.t.sol::test_seizedZeroSweepSkipped.","severity":"low","snippet":"            quote.budget == quote.coveredAmount && seized != 0","title":"Collateral-limited dust sweep is skipped when seized floors to zero, leaving a position that neither liquidate nor finalizeDust can clear"},{"citation":"resolved","description":"configureRisk and RiskOracle.configure are governance-only, but GovernanceTimelock.execute has no caller restriction and the new ltv/threshold/feeds apply to existing positions at the moment of the write, with no activation grace or ramp. Any searcher can pick the exact block within the 7-day grace window, call execute and liquidate in one transaction, and capture the 8% bonus on positions whose HF was above 1 one call earlier with no price movement. Borrowers only get the 2-day notice and no on-chain signal before the parameter actually changes. ARCHITECTURE.md documents parameter changes as a trusted delayed power; this is reported because the open executor hands an unprivileged party control over timing and the liquidation is permissionless. Design-preserving mitigation: apply threshold/LTV reductions after a short activation delay stored with the pending values, or restrict execute of such operations to the proposer.","line":389,"path":"src/IMDBank.sol","reproduction":"State: GovernanceTimelock(proposer=this, canceller=0x1234, 2 days) governing a fresh IMDBank; ltv 2500 / threshold 3500, IMD $10, Alice supplies 1000e18 IMD and borrows 2000e6 USDC (HF 1.75). Proposer schedules configureRisk(1000, 1500, 800, 5000, 1e24) (within hard bounds). After 2 days SEARCHER calls timelock.execute(...) then bank.liquidate(Alice, USDC, max, 0, now) in the same transaction: HF reads 0.75, liquidation repays 2000.221383e6 USDC and seizes 216.023909364e18 IMD (8% bonus) although no price changed. Reproduced in test/scratch/Repro.t.sol::test_paramRace.","severity":"low","snippet":"        liquidationThresholdBps = threshold;","title":"Risk-parameter cuts apply atomically on permissionless timelock execution, so any searcher can execute and liquidate positions that were healthy one call earlier in the same transaction"},{"citation":"resolved","description":"projectedHealth reproduces accountData's threshold-based health factor (35%), and app.js line 218 only marks the projection as dangerous when it is below 1.0. IMDBank._requireBorrowSafe rejects borrow and debt-bearing withdraw when debtUsd > borrowCapacityUsd, which uses ltvBps (25%). Every borrow or withdrawal that leaves the account between HF 1.0 and HF 1.4 (threshold/LTV) is displayed as a non-danger projection and then fails with UnsafePosition at simulation. The review dialog's pre-checks (cash, supplied balance, wallet balance) do not include capacity either. No funds are at risk because the contract is the source of truth; the displayed number cannot tell users the real rejection boundary. Fix: compute remaining capacity (ltvBps) in projectedHealth or review() and flag actions whose projected debt exceeds projected collateral * ltvBps / 10000.","line":68,"path":"web/core.js","reproduction":"Snapshot: collateralUsd 10000e18, debtUsd 0, threshold 3500, ltv 2500, IMD price 10e18, enabled true. Borrow form: 3000 USDC (amount 3000e6, decimals 6, price 1e18). projectedHealth returns 10000e18*3500/10000*1e18/3000e18 = 1.1666e18, rendered as 'Projected health factor: 1.166' without the danger class. Contract: capacity = 2500e18, debtUsd = 3000e18 > capacity, borrow reverts UnsafePosition. Same for withdraw: with debt 2500e18, withdrawing 1 IMD projects HF 1.3986 (safe) and reverts on chain.","severity":"low","snippet":"  return d === 0n ? MaxUint256 : c * liquidationBps / 10000n * USD / d;","title":"Frontend projected health factor omits the LTV capacity check the contract enforces on borrow and withdraw, so a projection shown as safe is rejected on chain"},{"citation":"resolved","description":"The brief requires the frontend to be hosted as a usable public website with a final public HTTPS URL and states deployment is not complete if the frontend only runs locally. docs/DEPLOYMENT.md records that no public URL exists, web/config.json has bankAddress/oracleAddress/code hashes null with deploymentStatus NOT_DEPLOYED, and WalletConnect is documented as not implemented. This is a completeness gap against the stated acceptance criteria rather than a code defect; the frontend correctly refuses to invent addresses when unconfigured.","line":5,"path":"docs/DEPLOYMENT.md","reproduction":"State: web/config.json as committed (bankAddress null, oracleAddress null, bankCodeHash null, oracleCodeHash null, deploymentStatus 'NOT_DEPLOYED'). Loading web/index.html runs validateConfig in web/core.js which throws 'Protocol deployment is not configured. Transactions are unavailable.' and all [data-write] controls stay disabled. Expected per brief: a public HTTPS URL where a wallet can connect and execute protocol transactions. Actual: grep for 'https://' in docs/DEPLOYMENT.md yields only hosting guidance; no URL exists in the tree.","severity":"info","snippet":"**Public HTTPS URL: not deployed / unavailable. Mainnet application addresses: not deployed.**","title":"Required public HTTPS frontend deployment is absent; the repository states the website is not deployed and web/config.json is unpopulated"},{"citation":"resolved","description":"The manifest's second constructor argument for GovernanceTimelock, RiskOracle and IMDBank is the zero address. GovernanceTimelock's constructor reverts InvalidOperation when canceller_ == address(0) (src/GovernanceTimelock.sol line 25), and RiskOracle/IMDBank revert InvalidConfiguration when guardian_ == address(0) (src/RiskOracle.sol line 44, src/IMDBank.sol line 141). The notes field already declares the manifest a blocked draft awaiting an authorized emergency multisig; recorded here so the judge tracks that the deployment prerequisite is unmet and that the replacement address must be distinct from $owner (finding 3 additionally recommends it be distinct from the timelock canceller).","line":8,"path":"launch.json","reproduction":"Deploy the manifest as written: new GovernanceTimelock($owner, 0x0, 172800) reverts InvalidOperation() at the canceller_ == address(0) check, so $contract:GovernanceTimelock never resolves and RiskOracle/IMDBank (which would also revert on guardian_ == address(0)) are never reached. Expected: a deployable manifest with a nonzero, authorized emergency address. Actual: factory rehearsal fails at the first constructor.","severity":"info","snippet":"        \"0x0000000000000000000000000000000000000000\",","title":"launch.json passes the zero address as timelock canceller and as oracle/bank guardian, which all three constructors reject, so the manifest cannot deploy"}],"hash":"4500c54fcbb5d2d47d51da4470eb512c08ca733ea0ccede2f1761e246e61abc1","nodeId":"ab94eb68-e441-4a76-9d3d-3625d37e9583","outcome":"completed","summary":"All snippets match the tree, the four embedded proofs fail on the current code, and `.imd-findings.json` is in place. Nothing outside test/scratch and the findings file was changed.\n\n**Outcome:** 11 findings written to `.imd-findings.json` with a 21-row coverage record (all 20 entry points plus one invariant). No critical or high defects reproduced. Four mediums carry failing Foundry proofs.\n\n**Findings kept, after reproduction and merging**\n\n- **Medium, oracle band** (merged economics + math): a valid two-feed crash below the mandatory collateral `minPrice` makes liquidate, finalizeDust, borrow and debt-bearing withdraw revert. Proof fails with `InvalidPrice()`.\n- **Medium, no minimum position** (merged math + permissions): a 1-unit USDC debt doubles after one second of ceil-rounded accrual, sits in a band unreachable by both liquidate and finalizeDust, then a $0.000002 loss freezes the whole bank and can front-run a matured unfreeze. Proof fails.\n- **Medium, immutable oracle guardian + canceller wiring**: the emergency key can veto every governance reversal and re-disable the IMD feed forever. Proof fails.\n- **Medium, 1-day maxAge cap** (merged economics + flow): equals the selected USDT/USD heartbeat, so stablecoin-debt positions are unliquidatable in a daily window. I wrote a fix-agnostic proof that fails with `InvalidPrice()`.\n- **Low**: supply-cap fill griefing, cross-side feed pricing inflating the liquidator premium, zero-seize sweep gap, atomic parameter cut via permissionless execute, frontend projection omitting LTV capacity.\n- **Info**: no public HTTPS deployment; launch.json zero-address roles that all three constructors reject.\n\n**Dropped:** the economics specialist's invariant-coverage claim, because `test/ThreeReserveInvariants.t.sol` already exercises multi-asset borrow, finalizeDust and coverBadDebt. The alternative floor proof that hard-codes `minPrice == 0` as the fix was replaced by the fix-agnostic one.\n\n**My own pass** over the remaining entry points (schedule, accrue, repay, donate, coverBadDebt, configureReserve, setCollateralEnabled, setReserveFrozen, withdraw) found no additional defects. Each row carries a one-line reason.","treeHash":null,"usage":{"cachedInputTokens":1444526,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":49392,"runtime":"claude","turns":33,"wallClockMs":600873}},{"artifacts":[],"attempt":1,"bundleHash":"1d7cc3051cad4c48435ade87a26a7f1422cef0cb85551526da090f538934d2a7","device":"5b85781dd6c37cca","findings":[],"hash":"49a36f82c701b679436131675cfb5e0922bb5793a63237dd8c2918033ef9ec7d","nodeId":"9c671217-3539-43e9-875a-0ca6c0607088","outcome":"completed","summary":"Added failure-path, fuzz, oracle, token, governance, and three-reserve invariant tests under `test/`.\n\n- `forge build`: passed.\n- `forge test`: 90 passed, 0 failed.\n- Invariants: 24,576 randomized calls.\n- Fork suite: skipped because no RPC was configured.\n\nNo new reproducible contract defects found. Coverage and limitations are documented in `test/ADDED_TEST_COVERAGE.md`.","treeHash":"71a0723d5d795fa4670401cdf39e6d81dac9142a","usage":{"cachedInputTokens":1916928,"inputTokens":114209,"model":"gpt-6-astra","outputTokens":35432,"runtime":"codex","turns":9,"wallClockMs":1103818}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"523ef565dd740e25","findings":[{"citation":"resolved","description":"Trust gap (access x asymmetry). The documented model is: guardian may freeze and disable feeds, only governance may unfreeze/re-enable, and the canceller 'can veto, not execute or replace' (docs/ARCHITECTURE.md). In the production wiring the same emergencyMultisig is (a) IMDBank.guardian, (b) RiskOracle.guardian, and (c) GovernanceTimelock.canceller. GovernanceTimelock.cancel (src/GovernanceTimelock.sol:49) lets the canceller cancel any pending operation, and IMDBank.governor / RiskOracle.governor are the timelock, so every reversal (RiskOracle.setEnabled(asset,true), IMDBank.setFrozen(false), IMDBank.setGuardian(new)) must pass through a 2-day window during which the emergency key cancels it. Even if a reversal executed, RiskOracle.guardian is immutable (src/RiskOracle.sol:32) and RiskOracle.setEnabled(asset,false) is instant, so the key can re-disable the collateral feed in the next block forever; IMDBank.setGuardian only rotates the bank guardian and gives a false sense of rotation. While the IMD feed is disabled, IMDBank._price reverts for liquidate, finalizeDust, borrow and debt-bearing withdraw. A compromised or rogue 2-of-3 emergency multisig therefore converts its 'emergency pause' into a permanent state: unhealthy positions cannot be liquidated while collateral falls (reserve donors absorb unbounded bad debt that cannot even be recognized), and borrowers cannot withdraw excess collateral without full repayment. GovernanceTimelock's own NatSpec says proposer and canceller should be distinct reviewed multisigs; the deployment contradicts it by reusing the guardian as canceller. Severity is medium because it needs a privileged actor, but it is a material privileged-power risk that the docs state the opposite of. Fix options that preserve the design: make the timelock canceller a distinct address from the bank/oracle guardian (or the proposer alone), and give governance a timelocked way to rotate RiskOracle.guardian (mirroring IMDBank.setGuardian) so that an emergency disable is always reversible by governance.","line":35,"path":"script/DeployMainnet.s.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDBank} from \"src/IMDBank.sol\";\nimport {RiskOracle} from \"src/RiskOracle.sol\";\nimport {GovernanceTimelock} from \"src/GovernanceTimelock.sol\";\n\ncontract VetoToken {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        if (allowance[from][msg.sender] != type(uint256).max) allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract VetoFeed {\n    uint8 public decimals = 8;\n    int256 public answer;\n\n    constructor(int256 a) {\n        answer = a;\n    }\n\n    function set(int256 a) external {\n        answer = a;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, answer, block.timestamp, block.timestamp, 1);\n    }\n}\n\ncontract Multisig {}\n\n/// @notice Production wiring: timelock(proposer=governance, canceller=emergency), oracle(timelock, emergency),\n/// bank(timelock, emergency). The emergency key can hold every price-dependent path closed forever.\ncontract GuardianVetoTest is Test {\n    VetoToken imd;\n    VetoToken usdc;\n    VetoToken usdt;\n    VetoToken weth;\n    VetoFeed imdA;\n    VetoFeed imdB;\n    GovernanceTimelock timelock;\n    RiskOracle oracle;\n    IMDBank bank;\n    address governance;\n    address emergency;\n    address constant ALICE = address(0xA11CE);\n    address constant LIQUIDATOR = address(0xCAFE);\n\n    function setUp() public {\n        vm.warp(100 days);\n        governance = address(new Multisig());\n        emergency = address(new Multisig());\n        imd = new VetoToken(18);\n        usdc = new VetoToken(6);\n        usdt = new VetoToken(6);\n        weth = new VetoToken(18);\n        timelock = new GovernanceTimelock(governance, emergency, 2 days);\n        oracle = new RiskOracle(address(timelock), emergency);\n        bank = new IMDBank(address(timelock), emergency, address(imd), address(oracle), address(usdc), address(usdt), address(weth));\n\n        imdA = new VetoFeed(10e8);\n        imdB = new VetoFeed(10e8);\n        VetoFeed usdA = new VetoFeed(1e8);\n        VetoFeed usdB = new VetoFeed(1e8);\n        VetoFeed ethA = new VetoFeed(2000e8);\n        VetoFeed ethB = new VetoFeed(2000e8);\n        _govern(address(oracle), abi.encodeCall(oracle.configure, (address(imd), address(imdA), address(imdB), 1 days, 1 days, 500, 1e15, 1e24, true)), 0);\n        _govern(address(oracle), abi.encodeCall(oracle.configure, (address(usdc), address(usdA), address(usdB), 1 days, 1 days, 500, 1e15, 1e24, false)), 1);\n        _govern(address(oracle), abi.encodeCall(oracle.configure, (address(usdt), address(usdA), address(usdB), 1 days, 1 days, 500, 1e15, 1e24, false)), 2);\n        _govern(address(oracle), abi.encodeCall(oracle.configure, (address(weth), address(ethA), address(ethB), 1 days, 1 days, 500, 1e15, 1e24, false)), 3);\n        _govern(address(bank), abi.encodeCall(bank.configureRisk, (2500, 3500, 800, 5000, 1_000_000e18)), 4);\n        _govern(address(bank), abi.encodeCall(bank.configureReserve, (address(usdc), 1_000_000e6, 0.02e27, 0.08e27, 0.9e27, 8000)), 5);\n        _govern(address(bank), abi.encodeCall(bank.setFrozen, (false)), 6);\n\n        usdc.mint(address(this), 1_000_000e6);\n        usdc.approve(address(bank), type(uint256).max);\n        bank.donateLiquidity(address(usdc), 1_000_000e6);\n        imd.mint(ALICE, 1000e18);\n        vm.startPrank(ALICE);\n        imd.approve(address(bank), type(uint256).max);\n        bank.supply(1000e18, ALICE);\n        bank.setCollateralEnabled(true);\n        bank.borrow(address(usdc), 2000e6, ALICE);\n        vm.stopPrank();\n        usdc.mint(LIQUIDATOR, 1_000_000e6);\n        vm.prank(LIQUIDATOR);\n        usdc.approve(address(bank), type(uint256).max);\n    }\n\n    function _govern(address target, bytes memory data, uint256 salt) internal {\n        vm.prank(governance);\n        timelock.schedule(target, data, bytes32(salt));\n        vm.warp(block.timestamp + 2 days);\n        timelock.execute(target, data, bytes32(salt));\n    }\n\n    /// @dev Fails on the current tree: the emergency key is both guardian and canceller, so every governance\n    /// operation that would reverse the guardian's action (re-enable the feed, unfreeze, rotate the guardian)\n    /// is vetoed by the same key, and the oracle guardian is immutable anyway. Liquidation stays impossible.\n    function test_emergencyKeyCannotPermanentlyBlockLiquidations() public {\n        // 1. Emergency key disables the collateral feed and freezes the bank (allowed, instant).\n        vm.startPrank(emergency);\n        oracle.setEnabled(address(imd), false);\n        bank.setFrozen(true);\n        vm.stopPrank();\n\n        // 2. IMD crashes 60%; Alice's position is deeply unhealthy but no one can liquidate: Disabled().\n        imdA.set(4e8);\n        imdB.set(4e8);\n        vm.prank(LIQUIDATOR);\n        vm.expectRevert(RiskOracle.Disabled.selector);\n        bank.liquidate(ALICE, address(usdc), type(uint256).max, 0, block.timestamp);\n\n        // 3. Governance tries every reversal; the emergency key cancels each one before it matures.\n        bytes memory reenable = abi.encodeCall(oracle.setEnabled, (address(imd), true));\n        bytes memory unfreeze = abi.encodeCall(bank.setFrozen, (false));\n        bytes memory rotate = abi.encodeCall(bank.setGuardian, (address(0xD00D)));\n        vm.startPrank(governance);\n        bytes32 id1 = timelock.schedule(address(oracle), reenable, bytes32(uint256(100)));\n        bytes32 id2 = timelock.schedule(address(bank), unfreeze, bytes32(uint256(101)));\n        bytes32 id3 = timelock.schedule(address(bank), rotate, bytes32(uint256(102)));\n        vm.stopPrank();\n        vm.startPrank(emergency);\n        timelock.cancel(id1);\n        timelock.cancel(id2);\n        timelock.cancel(id3);\n        vm.stopPrank();\n        vm.warp(block.timestamp + 2 days);\n        vm.expectRevert(GovernanceTimelock.NotReady.selector);\n        timelock.execute(address(oracle), reenable, bytes32(uint256(100)));\n        vm.expectRevert(GovernanceTimelock.NotReady.selector);\n        timelock.execute(address(bank), unfreeze, bytes32(uint256(101)));\n        vm.expectRevert(GovernanceTimelock.NotReady.selector);\n        timelock.execute(address(bank), rotate, bytes32(uint256(102)));\n\n        // 4. Even if a reversal slipped through, the oracle guardian is immutable and re-disables instantly.\n        assertEq(oracle.guardian(), emergency);\n\n        // Expected: some governance path restores liquidation. Actual: none exists.\n        (,,,, uint256 hf) = _safeAccountData(ALICE);\n        bool liquidatable;\n        vm.prank(LIQUIDATOR);\n        try bank.liquidate(ALICE, address(usdc), type(uint256).max, 0, block.timestamp) {\n            liquidatable = true;\n        } catch {}\n        assertTrue(liquidatable, \"liquidation permanently blocked by emergency key\");\n        hf;\n    }\n\n    function _safeAccountData(address account) internal view returns (uint256, uint256, uint256, uint256, uint256) {\n        try bank.accountData(account) returns (uint256 a, uint256 b, uint256 c, uint256 d, uint256 e) {\n            return (a, b, c, d, e);\n        } catch {\n            return (0, 0, 0, 0, 0);\n        }\n    }\n}","reproduction":"State: production wiring from DeployMainnet (timelock(proposer=G, canceller=E), oracle(timelock,E), bank(timelock,E)), IMD feeds at $10, Alice supplies 1000 IMD and borrows 2000 USDC. Steps: (1) E calls oracle.setEnabled(IMD,false) and bank.setFrozen(true). (2) Both IMD feeds fall to $4 (HF ~0.7). (3) LIQUIDATOR calls bank.liquidate(Alice, USDC, max, 0, now) -> reverts RiskOracle.Disabled(). (4) G schedules oracle.setEnabled(IMD,true), bank.setFrozen(false), bank.setGuardian(0xD00D); E calls timelock.cancel on all three ids; after 2 days each execute reverts NotReady. (5) oracle.guardian() is still E and cannot be changed by anyone. Expected: governance can always restore liquidation after an emergency action. Actual: liquidation and bad-debt recognition are blocked for as long as E wishes; test/scratch/GuardianVeto.t.sol fails on the current tree with 'liquidation permanently blocked by emergency key'.","severity":"medium","snippet":"        timelock = new GovernanceTimelock(governanceMultisig, emergencyMultisig, 2 days);\n        oracle = new RiskOracle(address(timelock), emergencyMultisig);\n        bank = new IMDBank(address(timelock), emergencyMultisig, IMD, address(oracle), USDC, USDT, WETH);","title":"Emergency key is timelock canceller and immutable oracle guardian: it can veto every governance reversal and keep liquidations blocked indefinitely"},{"citation":"resolved","description":"Trust gap (access x economics, race amplifier). configureRisk and RiskOracle.configure are governance-only, but GovernanceTimelock.execute (src/GovernanceTimelock.sol:55) has no caller restriction and the new ltv/threshold/feeds apply to existing positions at the moment of the write, with no post-execution grace period or ramp. Any searcher can pick the exact block within the 7-day grace window, call execute and liquidate in the same transaction, capturing the 8% bonus on positions whose health factor was above 1 one call earlier with no price movement. Borrowers only had the 2-day notice window and no on-chain signal before the parameter actually changes. This is documented as a trusted power in docs/ARCHITECTURE.md; it is reported because the open executor gives an unprivileged party control over the timing and the liquidation is permissionless. A design-preserving mitigation is to have configureRisk apply threshold/LTV reductions after a short grace delay (e.g. store pending values with an activation timestamp), or to require governance to execute such operations itself.","line":389,"path":"src/IMDBank.sol","reproduction":"State: ltv 2500 / threshold 3500, IMD $10, Alice supplied 1000 IMD and borrowed 2000 USDC (HF 1.75). Governance schedules configureRisk(1000,1500,800,5000,1e24) (all within hard bounds). After 2 days SEARCHER calls timelock.execute(...) then bank.liquidate(Alice, USDC, max, 0, now) in the same tx: HF reads 0.75, liquidation pays ~2000.22 USDC and seizes ~216.02 IMD (8% bonus) although no price changed. Expected: a parameter change cannot by itself make a position liquidatable in the same block as its activation. Actual: test/scratch/ParamRace.t.sol::test_openExecutorAtomicallyLiquidatesAfterThresholdCut demonstrates the atomic sequence.","severity":"low","snippet":"        liquidationThresholdBps = threshold;","title":"Risk-parameter cuts take effect atomically with permissionless timelock execution; executor can liquidate positions that were healthy one call earlier"},{"citation":"resolved","description":"Access asymmetry: frozen=true is otherwise reachable only by governor/guardian (_freezeAuthority), but _writeOff, reachable from the permissionless finalizeDust and liquidate, sets it for everyone, and setFrozen(false) (src/IMDBank.sol:419-428) then reverts OutstandingBadDebt until the loss is covered. The author documents this as the retained M-05 residual. The additional point for the trust-gap area is the interaction with the timelock: an unfreeze is a 2-day scheduled operation and the griefer can front-run its execute with finalizeDust on another pre-positioned insolvent micro-account, so the matured execute reverts (ExecutionFailed(OutstandingBadDebt)), and repeated reverts can exhaust the 7-day grace window, forcing a reschedule. Cost to the griefer is 4e11 wei IMD (~$0.000004 at $10) plus 1 USDC base unit per account, created while unfrozen, and a later ~75% IMD price drop makes all of them finalizable at once. Governance can only defend by enumerating and finalizing/covering every such account before executing. A minimum economic position size, or recording tiny losses without flipping the global flag (bounded dust reserve), would remove the permissionless write of the governance-only flag.","line":509,"path":"src/IMDBank.sol","reproduction":"State: bank unfrozen, IMD $10, reserves funded. Griefer supplies 4e11 wei IMD, enables collateral, borrows 1 unit USDC. IMD falls to $2.40 (collateral $0.00000096 < debt $0.000001; cannot repay one unit at 8% bonus). Anyone calls finalizeDust(griefer): badDebt(USDC)=1, frozen()==true, borrowing halts for all users. Governance schedules setFrozen(false); after 2 days execute reverts with ExecutionFailed(OutstandingBadDebt()). test/scratch/ParamRace.t.sol::test_anyoneCanGloballyFreezeLendingForMicroCost passes on the current tree, demonstrating the state.","severity":"low","snippet":"            reserve.frozen = true;\n            frozen = true;","title":"Permissionless finalizeDust writes the governance-only frozen flag; a pre-positioned micro-account can fail a matured setFrozen(false) proposal"}],"hash":"59fe3089536337805dd78bbfcbaef4b53e1d984cb74c8abb67b6c11856daf283","nodeId":"3c5549ef-1b46-43cf-8498-f88f7a849b1f","outcome":"completed","summary":"The review is complete. The findings file is written and validated, and no tracked source file was changed.\n\n**Outcome.** The permission model of all three contracts is sound at the function level: every guard I traced (`onlyGovernor`, the governor/guardian freeze split, the oracle's disable-only guardian, the timelock's proposer/canceller checks, replay and expiry handling) does what it claims, and the 47 existing tests pass. The defects I found live in how the roles are wired together rather than in any single guard.\n\n**Findings written to `.imd-findings.json`:**\n\n1. **Medium, trust gap.** The production wiring in the deploy script makes the emergency multisig the bank guardian, the oracle guardian, and the timelock canceller at once. The guardian can disable the IMD feed and freeze the bank, then cancel every governance proposal that would re-enable, unfreeze, or rotate it. The oracle guardian is immutable, so even an executed re-enable can be undone the next block. Result: a rogue or compromised emergency key blocks liquidation and bad-debt recognition indefinitely, which contradicts the documented \"only governance can reverse\" model and the timelock's own NatSpec. A scratch test reproduces this and fails on the current tree.\n\n2. **Low, trust gap with race amplifier.** Risk-parameter cuts apply to open positions in the same call as their activation, and timelock execution is permissionless. A searcher can execute a threshold cut and liquidate in one transaction. Reproduced: HF moves from 1.75 to 0.75 with no price change, and the executor collects the 8% bonus.\n\n3. **Low, access asymmetry.** The governance-only frozen flag is also written by the permissionless dust write-off path. This is the author's retained M-05 residual. I added the timelock interaction: a pre-positioned micro-account can be finalized just before a matured unfreeze executes, making it revert and burning the grace window.\n\n**Coverage.** All 20 verifier entry points have a row plus three invariant rows. Fourteen hold, six carry a finding reference, none are unreached. Items outside my area I did not pursue in depth: oracle feed-level arithmetic and the frontend beyond checking that approvals are exact-amount with a zero reset.","treeHash":null,"usage":{"cachedInputTokens":1887106,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":54030,"runtime":"claude","turns":40,"wallClockMs":842272}},{"artifacts":[],"attempt":2,"bundleHash":"866b2b2573a0a9719d31b900b6e69c2c507a66edffb360af2cd7ae8bfe27f589","device":"75474cc326e8a817","findings":[],"hash":"5ef5b3838f0f7f9d184d038dce724fabe20e06d28b237484ead0bf5879f3ed30","nodeId":"a60bb6c7-d7af-4e2b-8d6a-c9af2af09573","outcome":"completed","summary":"Created [launch.json](/root/nft7/.identitymd/work/464d2b15-5c49-45a4-8834-3c62c29e3e23/a60bb6c7-d7af-4e2b-8d6a-c9af2af09573/launch.json) as an explicitly blocked draft. It needs an authorized emergency guardian/canceller address distinct from `$owner`; the current zero placeholders intentionally prevent deployment.\n\n`forge build` passed with warnings; all 47 tests passed. Only `launch.json` is a submission change. Deployment acceptance remains unmet pending that address.","treeHash":"c14e412d8eb9dc7c924f12f033c2ffd3c6876377","usage":{"cachedInputTokens":287616,"inputTokens":79450,"model":"gpt-6-astra","outputTokens":6037,"runtime":"codex","turns":5,"wallClockMs":235218}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c9668a6c89d33b13","findings":[],"hash":"6aa00a027bd78ac2dd05bef493984653c3a519332a2147a292b7d280c106f6f3","nodeId":"4c73704e-c92e-4ec4-97eb-286b5993aa58","outcome":"failed","summary":"Your workspace is out of credits. Ask your workspace owner to refill in order to continue.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":3,"wallClockMs":836124}},{"artifacts":[],"attempt":1,"bundleHash":"a8df1a27c663f6ec376110f0a768490cce75000d39ac33577152b064664b8a43","device":"8f87b0b1cd164f2b","findings":[],"hash":"7cd29902101eb209fb89c69a9dfc06705560795999f59b6fbd85d05c161f4e63","nodeId":"a60bb6c7-d7af-4e2b-8d6a-c9af2af09573","outcome":"completed","summary":"Reproduced the failure, corrected `launch.json`’s role notes, and recorded the unresolved finding in `.imd-responses.json`.\n\n`forge build` passed with warnings; all 56 tests passed.\n\nThe manifest remains non-deployable. Completion requires authorized canceller and guardian addresses, distinct from each other and `$owner`; none were supplied.","treeHash":"b3165e9ec7788d4bbda347091b61e481149b6f38","usage":{"cachedInputTokens":355328,"inputTokens":52119,"model":"gpt-6-astra","outputTokens":5776,"runtime":"codex","turns":5,"wallClockMs":267050}},{"artifacts":[],"attempt":1,"bundleHash":"c2e7933221a6e80872118673ded0c5b1753bce90741d11aeb5f033cb64bf1f02","device":"9a29c101a23c14cc","findings":[],"hash":"7f8db58646b1b068377bc70993767061dea354f2e52da4526f1ee9a0520aebb3","nodeId":"9c671217-3539-43e9-875a-0ca6c0607088","outcome":"completed","summary":"Updated tests only under `test/`, preserving existing coverage and adding regressions for minimum debt, loss halts, oracle price moves, and guardian-pause expiry.\n\n- `forge build`: passed.\n- `forge test`: **101 passed, 0 failed, 1 skipped**.\n- Invariants: **24,576 calls, zero handler reverts**.\n\nMainnet fork remains unverified without an RPC. No new reproducible contract defects found.","treeHash":"7ef643cb5b94ea5843549e667e1f777b2d5d5351","usage":{"cachedInputTokens":1396352,"inputTokens":243702,"model":"gpt-6-astra","outputTokens":12698,"runtime":"codex","turns":5,"wallClockMs":537847}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ac55933908aba3e5","findings":[],"hash":"84d142c8237e910cf4ac5851cd57778163b137e8d168dc6a69d9b914ea973751","nodeId":"f91b7e43-3fe9-4ce7-a2f4-af40e9f4938e","outcome":"failed","summary":"runtime reported <synthetic>, not the required premium model claude-fable-5-1","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"<synthetic>","outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":2666}},{"artifacts":[],"attempt":1,"bundleHash":"d3ae7a425afc52f763ff6032cd08adb0630201a478db448cf4164629d0f424cc","device":"3baa2091028c32c1","findings":[],"hash":"859d72692eec08f9913f88c9a85f730008c30531b947a3731409444751a95ed3","nodeId":"4c73704e-c92e-4ec4-97eb-286b5993aa58","outcome":"completed","summary":"All checks pass on the revised tree, so the revision is complete.\n\n**Outcome.** All four reproducible findings reproduced on the starting tree (each proof failed) and are fixed. Every proof now passes, the committed suite passes, the formatter passes, and the responses file is written.\n\n| Check | Result |\n|---|---|\n| `forge test` (committed suite, no scratch) | 52 passed, 0 failed |\n| Reviewer proofs under `test/scratch/` | 4 passed |\n| `forge build`, `forge fmt --check` | pass |\n| Frontend unit tests, ABI compatibility, bytecode policy | 13 passed, 34 signatures matched, no forbidden opcodes |\n| Foundry fork tests and frontend fork replay | 3 passed, 46 receipts at fork block 26,134,418 |\n\n**What changed, by finding**\n- **Oracle band (R2-01):** bounds now reject only the direction that overvalues a borrower. Collateral feeds are capped by `maxPrice`, debt feeds floored by `minPrice`, and a zero floor is allowed.\n- **Minimum debt and dust losses (R2-02):** borrowing must leave at least $1 of debt per asset. Write-offs only halt the reserve and bank when a reserve's recorded loss exceeds $1. Unfreeze gates use that halt flag, so dust finalizations can no longer block a scheduled unfreeze.\n- **Guardian powers (R2-03):** a guardian feed pause is time-boxed and single-use until governance re-arms it, governance can rotate the oracle guardian, and the deploy rehearsal requires three distinct multisigs with the veto key separate from the guardian.\n- **Feed age (R2-04):** the per-feed maximum age bound is now 48 hours so heartbeat plus grace can be configured.\n- **Advisories:** the zero-wei seizure sweep and the frontend LTV capacity check are fixed with regressions. The supply-cap fill, feed-spread premium and parameter-race items were reproduced in scratch tests but kept as design, with the trade-offs documented. The public hosting gap stays open because hosting credentials and a confirmed address set are not available here.\n\n**Documentation and evidence.** ARCHITECTURE, DEPLOYMENT, SECURITY_REVIEW (new round-2 section), VALIDATION, FRONTEND and README are updated. Evidence files were regenerated, and the Foundry fork test was re-pinned to block 26,134,418 because the public RPC no longer serves the old block's state.\n\n**Not done.** No public HTTPS URL exists. The fork validation ran only on a local Anvil fork, and no browser automation or WalletConnect connector was added.","treeHash":"a79eded935b7c3e0c0c3d3ac65a4e025e95126c3","usage":{"cachedInputTokens":6014570,"inputTokens":994,"model":"claude-fable-5-1","outputTokens":103080,"runtime":"claude","turns":118,"wallClockMs":1312673}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"68fd8dd6450ac70c","findings":[],"hash":"877043b30d84f0dba91d10bb288b100ec1926ca90d9af89139618b82283d05af","nodeId":"a60bb6c7-d7af-4e2b-8d6a-c9af2af09573","outcome":"failed","summary":"the task produced no changes; the agent's last message was: `launch.json` is blocked by one missing input: the authorized emergency guardian/canceller address. `GovernanceTimelock` requires it to differ from `$owner`; duplicate or zero addresses revert.\n\nPlease provide that Ethereum Mainnet address.\n\n`forge build` passed with lint warnings; all 47 existing tests and two scratch checks passed. No submission files were changed.","treeHash":null,"usage":{"cachedInputTokens":495616,"inputTokens":49987,"model":"gpt-6-astra","outputTokens":3641,"runtime":"codex","turns":5,"wallClockMs":212201}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"6208734cdf5317a1","findings":[{"citation":"resolved","description":"RiskOracle.configure rejects any primaryMaxAge/secondaryMaxAge above 86,400 s, and RiskOracle._read (line 120) reverts InvalidPrice when `block.timestamp - updated > maxAge`. The project's own research (docs/RESEARCH.md table 'Oracle selection and protections' and docs/evidence/chainlink-mainnet-catalog.json) selects the canonical Chainlink USDT/USD proxy 0x3E7d1eAB13ad0104d2750B8863b489D65364e32D, whose catalog heartbeat is exactly 86,400 s with a 0.25% deviation trigger. A stablecoin feed normally refreshes only at the heartbeat, and the heartbeat round is mined some seconds after the heartbeat elapses, so the latest round is routinely 86,401+ s old for part of every day. During that window `price(USDT)` reverts. Because IMDBank.accountData prices every reserve the account owes, this blocks `liquidate` (any asset) for every account holding USDT debt, blocks `borrow` and debt-bearing `withdraw` for those accounts, and blocks `finalizeDust`. The same applies to USDC/USD (82,800 s heartbeat) with only one hour of slack. The hard constant therefore cannot be configured to the dependency it was researched for (heartbeat plus tolerance), and the outage lands precisely when a collateral crash coincides with the feed's quiet period. The deployment doc acknowledges the risk as 'must be reviewed' but the code admits no configuration that avoids it. Priority order in the brief puts liquidation reliability above UX; this is a liveness defect in the liquidation path, not an operational choice. Suggested minimal fix: raise the constructor/configure bound (e.g. allow up to 2 days, or heartbeat + grace) while keeping governance responsible for per-feed values, or let governance supply a per-feed grace that is bounded separately from the heartbeat.","line":67,"path":"src/RiskOracle.sol","reproduction":"Setup: RiskOracle with two USDT feeds (8 decimals) configured as `configure(USDT, feedA, feedB, 1 days, 1 days, 2000, 0.5e18, 2e18, false)`; `configure(..., 1 days + 1, ...)` reverts InvalidConfiguration so no larger value is possible. IMDBank with ltv 25%/threshold 35%, Alice supplies 1000 IMD at $10 and borrows 2500 USDT (HF 1.4). Both USDT feeds report updatedAt = t0. Warp to t0 + 86,401 (one second past the heartbeat, before the heartbeat round is mined); IMD feeds fresh at $5 so HF would be 0.7. Expected: liquidation of the underwater position succeeds. Actual: `oracle.price(USDT)` reverts InvalidPrice; `bank.accountData(ALICE)` reverts InvalidPrice; `bank.liquidate(ALICE, USDT, max, 0, now)` reverts InvalidPrice; `bank.liquidate(ALICE, USDC, max, 0, now)` also reverts InvalidPrice because accountData prices the USDT debt. Warping back to t0 + 86,400 the same round is accepted and HF < 1 is reported. Reproduced in test/scratch/Review.t.sol::test_usdtHeartbeatWindowBlocksLiquidation (passes, i.e. the reverts occur).","severity":"medium","snippet":"                || primaryMaxAge > 1 days || secondaryMaxAge > 1 days || maxDeviationBps == 0","title":"Oracle max-age hard bound of 1 day leaves zero slack for the 86,400 s USDT/USD heartbeat, so USDT pricing and every liquidation touching USDT debt fail in a recurring daily window"},{"citation":"resolved","description":"RiskOracle.price returns `low` for collateral-side feeds and `high` for debt-side feeds (src/RiskOracle.sol:112) and accepts disagreement up to maxDeviationBps (configurable to 2000). Using the conservative side for the health-factor check is defensible, but _liquidationQuote also uses the same two prices to compute how much collateral the liquidator receives: seizedValue = paid x debtPrice(high) x (1+bonus), seized = seizedValue / collateralPrice(low). With both pairs disagreeing inside the permitted band, a liquidator is paid (1+bonus)(1+devIMD)(1+devUSDC) of value measured at the other, equally valid, feed values. configureRisk caps the bonus at 15% and enforces `threshold*(BPS+bonus) < BPS*BPS` to preserve a buffer, but the effective transfer from borrower to liquidator can be ~1.65x, so the bound does not bound what borrowers actually lose. The borrower is additionally liquidatable at a mid-price HF of 1.4. This is borrower value loss under a specific but governance-permitted configuration; it does not threaten solvency (the covered-amount cap still keeps seized <= available). Suggested minimal fix (preserving the conservative HF rule): compute seizure with the same-side price for both legs (e.g. both `low` or both mid of the accepted pair) or cap the realized collateral at paid x (1+bonus) valued at the collateral feed's high side.","line":474,"path":"src/IMDBank.sol","reproduction":"Setup: RiskOracle with IMD feeds collateralSide=true and USDC feeds collateralSide=false, maxDeviationBps 2000; IMDBank ltv 25%/threshold 35%/bonus 8%/close factor 50%. Alice supplies 1000 IMD and borrows 2500 USDC with all feeds at $10 / $1 (accountData HF = 1.4e18). Then set IMD feed B = 8.4e8 (19.05% below A) and USDC feed B = 1.19e8 (19% above A); oracle.price(IMD) = 8.4e18, oracle.price(USDC) = 1.19e18, HF = 0.988e18. previewLiquidation(ALICE, USDC, max) returns paid = 1250e6 and seized = 191.25e18 (1250 x 1.19 x 1.08 / 8.4). Expected (per the documented 8% bonus, max 15% bound): at most 1250 x 1.15 / 8.4 = 171.1 IMD or, valued at the honest feeds, 135 IMD. Actual: 191.25 IMD transferred to the liquidator by `liquidate(ALICE, USDC, max, 191.25e18, now)`; at the other feed values that is $1,912.5 received for $1,250 paid, a 53% premium. Reproduced in test/scratch/Review.t.sol::test_feedSpreadInflatesLiquidatorPremium.","severity":"low","snippet":"            Math.mulDiv(Math.mulDiv(paid, quote.debtPrice, assetUnit[asset]), BPS + liquidationBonusBps, BPS);\n        seized = Math.min(quote.available, Math.mulDiv(seizedValue, collateralUnit, quote.collateralPrice));","title":"Liquidation seizure prices debt at the HIGH feed and collateral at the LOW feed, so the realized liquidator premium is bonus x (1+devCollateral) x (1+devDebt) and bypasses the 15% liquidationBonusBps "},{"citation":"resolved","description":"The brief requires the frontend to be hosted as a usable public website with a final public HTTPS URL and states deployment is not complete if the frontend only runs locally. docs/DEPLOYMENT.md line 5 records that no public URL exists, web/config.json has bankAddress/oracleAddress/code hashes set to null with deploymentStatus NOT_DEPLOYED, and WalletConnect is documented as not implemented. This is a completeness gap against the stated acceptance criteria rather than a code defect, and is outside this reviewer's control-flow area; it is recorded so the judge can track it. No code change is implied by this note; the frontend correctly refuses to invent addresses when unconfigured.","line":5,"path":"docs/DEPLOYMENT.md","reproduction":"State: web/config.json as committed (bankAddress null, oracleAddress null, bankCodeHash null, oracleCodeHash null, deploymentStatus 'NOT_DEPLOYED'). Loading web/index.html runs validateConfig (web/core.js:19) which throws 'Protocol deployment is not configured. Transactions are unavailable.' and all [data-write] controls remain disabled. Expected per brief: a public HTTPS URL where a wallet can connect and execute protocol transactions. Actual: no URL is provided anywhere in the tree (grep for 'https://' in docs/DEPLOYMENT.md yields only hosting guidance).","severity":"info","snippet":"**Public HTTPS URL: not deployed / unavailable. Mainnet application addresses: not deployed.** No wallet keys were read and no transactions were broadcast to a public chain. All state-changing integration activity used local Foundry or Anvil fork state. Hosting access, final role addresses and a confirmed deployment address set have not been supplied. A public website deployment must still be performed and verified; local serving and integration tests do not meet that criterion.","title":"Required public HTTPS frontend deployment is absent; the repository states the website is not deployed and the frontend config is unpopulated"}],"hash":"9aae0ba85080bbb6e2b8f6a8f4757de12e9a4ddbc8b8925b2daf3cd4af011b73","nodeId":"6fdf6ae3-4c22-4332-9035-c99d5dfdaf8f","outcome":"completed","summary":"The review is complete. The findings file is valid, every quoted snippet matches the tree at the cited line, and the working tree is clean apart from the required `.imd-findings.json` and the ignored `test/scratch/` directory.\n\n**What I reviewed.** I traced all 20 listed entry points across `IMDBank`, `RiskOracle` and `GovernanceTimelock`, plus the `ExactToken` library, through the Execution Trace, Periphery and First Principles lenses. The existing 47-test suite passes. I also read the frontend transaction path and the deployment and research docs for periphery context.\n\n**Findings written to `.imd-findings.json`:**\n\n- **Medium, `src/RiskOracle.sol:67`.** The oracle hard-caps feed max age at one day. The project's own research selects the Chainlink USDT/USD feed whose heartbeat is exactly 86,400 s, so there is zero slack. Each day, until the heartbeat round is mined, USDT pricing reverts, and because `accountData` prices every owed reserve, liquidation of any USDT debtor fails on every asset. Scratch test shows an HF 0.7 position that cannot be liquidated at heartbeat plus one second and can at heartbeat exactly.\n- **Low, `src/IMDBank.sol:474`.** Liquidation seizure uses the high debt feed and the low collateral feed. With both pairs disagreeing inside the permitted 20% band, a liquidator receives 191.25 IMD for 1,250 USDC, a 53% premium versus the 8% bonus and the 15% hard bound. Solvency is unaffected. The borrower bears the loss.\n- **Info, `docs/DEPLOYMENT.md:5`.** No public HTTPS frontend exists and the web config is unpopulated. This is a completeness gap against the brief, not a code defect, and is outside my area.\n\n**What held.** Indexed accounting and rounding direction, reentrancy ordering, freeze and bad-debt gates, dust finalization admission, timelock replay and atomicity, and the exact-delta token adapter all behaved as intended under the traces I ran. The static-analysis leads were sentinel equality, guarded timestamp use, and external calls made after the lock, none reproducible as defects.\n\n**Not reached.** Fork tests and browser or WalletConnect flows need network access that this environment lacks. Coverage rows record this explicitly.","treeHash":null,"usage":{"cachedInputTokens":2009513,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":62108,"runtime":"claude","turns":40,"wallClockMs":1040740}},{"artifacts":[],"attempt":2,"bundleHash":"64445b0c3e8157b62e1619b5e42254b9d37f87047aae98e0bd5767120f4ffc6b","device":"2e343a06f770172d","findings":[],"hash":"aaeadfb60c0255006f3fa8e8c0e4cee9feb92d7aa0f9f0783c216e8232dc3858","nodeId":"4c73704e-c92e-4ec4-97eb-286b5993aa58","outcome":"completed","summary":"Delivered contracts, wallet-connected frontend, cited research, tests, and deployment documentation. See the [project README](/home/seat/.identitymd/work/464d2b15-5c49-45a4-8834-3c62c29e3e23/4c73704e-c92e-4ec4-97eb-286b5993aa58/README.md).\n\nValidation passed: `forge build`, 47 Foundry tests, formatting, 12 frontend tests, three Mainnet fork tests, and 46 frontend integration transactions on a fork.\n\n**Production launch remains incomplete.** No public HTTPS URL exists. Approved IMD pricing, hosting access, WalletConnect, real-browser E2E, and external audit remain outstanding. One Medium availability finding remains documented. Supply/borrow caps stay zero and borrowing starts frozen.","treeHash":"a32dd3288e33ba83c363fa7d41f13b88e910029f","usage":{"cachedInputTokens":5982336,"inputTokens":122670,"model":"gpt-6-astra","outputTokens":41727,"runtime":"codex","turns":8,"wallClockMs":2077391}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4d71347e3f999162","findings":[{"citation":"resolved","description":"Boundary x invariant. RiskOracle.configure forces minPrice > 0 and maxPrice > minPrice for every feed, and price() reverts InvalidPrice whenever the lower of two agreeing, fresh feeds is below minPrice (or the higher is above maxPrice), regardless of which side the feed serves. IMDBank reads prices through _price() in accountData, _liquidationQuote and finalizeDust, so a collateral crash that crosses the floor turns every price-dependent entry point into a revert: liquidate, finalizeDust, borrow and debt-bearing withdraw all fail. Rejecting an under-floor collateral price is the unsafe direction: a lower collateral value can only make a borrower look worse, never better, and the two-feed deviation check already catches a broken single source. The invariant 'an account with HF < 1 can be liquidated while its collateral still covers 1.08 x debt' is lost for the whole time the price stays below the band, and the only repair is a governance reconfiguration through a 2-30 day timelock (the guardian can only disable feeds). docs/ARCHITECTURE.md recommends 'much narrower asset-specific bands', which widens the window in which an ordinary crash disables liquidations. Symmetrically, a debt-asset price above maxPrice (ETH spike) blocks liquidation of WETH borrowers.","line":107,"path":"src/RiskOracle.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDBank} from \"src/IMDBank.sol\";\nimport {RiskOracle} from \"src/RiskOracle.sol\";\n\ncontract BandToken {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address t, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[t] += a;\n        return true;\n    }\n}\n\ncontract BandFeed {\n    uint8 public constant decimals = 8;\n    int256 public answer;\n\n    constructor(int256 a) {\n        answer = a;\n    }\n\n    function set(int256 a) external {\n        answer = a;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, answer, block.timestamp, block.timestamp, 1);\n    }\n}\n\n/// @notice A valid, two-feed-agreed collateral price below the configured minPrice makes every\n/// price-dependent path revert, so an underwater borrower cannot be liquidated or finalized.\ncontract OracleBandBlocksLiquidationTest is Test {\n    address constant GUARDIAN = address(0xBEEF);\n    address constant ALICE = address(0xA11CE);\n    address constant LIQ = address(0xCAFE);\n\n    IMDBank bank;\n    RiskOracle oracle;\n    BandToken imd;\n    BandToken usdc;\n    BandToken usdt;\n    BandToken weth;\n    BandFeed imdA;\n    BandFeed imdB;\n\n    function setUp() public {\n        vm.warp(100 days);\n        imd = new BandToken(18);\n        usdc = new BandToken(6);\n        usdt = new BandToken(6);\n        weth = new BandToken(18);\n        oracle = new RiskOracle(address(this), GUARDIAN);\n        imdA = new BandFeed(10e8);\n        imdB = new BandFeed(10e8);\n        // Collateral band $5..$50 around the $10 reference price; stablecoin and WETH bands are generous.\n        oracle.configure(address(imd), address(imdA), address(imdB), 1 hours, 1 hours, 500, 5e18, 50e18, true);\n        oracle.configure(\n            address(usdc), address(new BandFeed(1e8)), address(new BandFeed(1e8)), 1 days, 1 days, 500, 0.9e18, 1.1e18, false\n        );\n        oracle.configure(\n            address(usdt), address(new BandFeed(1e8)), address(new BandFeed(1e8)), 1 days, 1 days, 500, 0.9e18, 1.1e18, false\n        );\n        oracle.configure(\n            address(weth),\n            address(new BandFeed(2000e8)),\n            address(new BandFeed(2000e8)),\n            1 hours,\n            1 hours,\n            500,\n            100e18,\n            100_000e18,\n            false\n        );\n        bank = new IMDBank(\n            address(this), GUARDIAN, address(imd), address(oracle), address(usdc), address(usdt), address(weth)\n        );\n        bank.configureRisk(2500, 3500, 800, 5000, 1e30);\n        bank.configureReserve(address(usdc), 1e30, 0.02e27, 0.08e27, 0.9e27, 8000);\n        bank.setReserveFrozen(address(usdc), false);\n        bank.setFrozen(false);\n        usdc.mint(address(this), 1_000_000e6);\n        usdc.approve(address(bank), type(uint256).max);\n        bank.donateLiquidity(address(usdc), 1_000_000e6);\n\n        imd.mint(ALICE, 1000e18);\n        vm.startPrank(ALICE);\n        imd.approve(address(bank), type(uint256).max);\n        bank.supply(1000e18, ALICE);\n        bank.setCollateralEnabled(true);\n        bank.borrow(address(usdc), 2500e6, ALICE); // $2,500 against $10,000 of IMD (max LTV)\n        vm.stopPrank();\n\n        usdc.mint(LIQ, 1_000_000e6);\n        vm.prank(LIQ);\n        usdc.approve(address(bank), type(uint256).max);\n    }\n\n    function test_collateralCrashBelowFloorMustStillBeLiquidatable() public {\n        // IMD crashes 60% to $4.00. Both independent feeds agree exactly; nothing is stale or broken.\n        imdA.set(4e8);\n        imdB.set(4e8);\n        // Collateral is now $4,000 against $2,500 debt: HF = 4000 * 0.35 / 2500 = 0.56.\n        // The position is deeply liquidatable and still fully recoverable (collateral > 1.08 * debt).\n        vm.prank(LIQ);\n        (uint256 repaid, uint256 seized) =\n            bank.liquidate(ALICE, address(usdc), type(uint256).max, 0, block.timestamp);\n        assertGt(repaid, 0, \"underwater borrower was not liquidatable\");\n        assertGt(seized, 0, \"no collateral seized\");\n        assertEq(bank.previewDebt(ALICE, address(usdc)), 0, \"full close should have been possible\");\n    }\n}","reproduction":"State: RiskOracle with IMD feeds at $10 (8 decimals), minPrice 5e18, maxPrice 50e18, deviation 500 bps, collateralSide true; USDC/USDT/WETH feeds in band; IMDBank with LTV 2500, threshold 3500, bonus 800; Alice supplies 1000e18 IMD ($10,000) and borrows 2500e6 USDC. Both IMD feeds move to 4e8 ($4.00, a 60% move, exact agreement, fresh). Call bank.liquidate(ALICE, USDC, type(uint256).max, 0, block.timestamp) from a funded liquidator. Expected: collateral $4,000 vs debt $2,500 gives HF 0.56 and a fully recoverable position (4000 > 1.08 * 2500), so the liquidation repays 2500e6 and seizes 675e18 IMD. Actual: reverts InvalidPrice() from RiskOracle.price because low (4e18) < minPrice (5e18). bank.finalizeDust(ALICE) and bank.accountData(ALICE) revert with the same error. At exactly 5e8 the same calls succeed with HF 0.70. Suggested minimal fix preserving the design: apply only the direction that would overvalue the borrower's position as a hard reject (collateral side: high > maxPrice; debt side: low < minPrice) and treat the other bound as a monitoring alert, or let the guardian lower a collateral floor without the timelock.","severity":"medium","snippet":"        if (\n            low < f.minPrice || high > f.maxPrice\n                || Math.mulDiv(high - low, 10_000, low, Math.Rounding.Ceil) > f.maxDeviationBps\n        ) revert InvalidPrice();","title":"Valid collateral price below the mandatory minPrice band makes liquidation and loss recognition revert exactly when they are needed"},{"citation":"resolved","description":"Three-way seam (boundary x precision x invariant). borrow() admits any amount >= 1 base unit, shares are minted at ceil(amount*RAY/index) and displayed debt is ceil(shares*index/RAY). With exactly one share at index RAY, the first accrual (index = RAY + ceil(0.02e27/365 days) = 1e27 + 634195839675292) makes ceil(1 * 1.000000000634e27 / 1e27) = 2: a one-unit debt becomes two units after one second, a 100% jump, which drops the health factor from 1.40 to 0.70 with no price move. For this position the ordinary liquidation path and the dust path do not overlap: _liquidationQuote needs coveredAmount = floor(collateralUsd/1.08 in debt units) >= 1 and burned = floor(budget*RAY/index) >= 1 (index > RAY means budget 1 burns nothing), while finalizeDust needs ceil(collateralUsd) < debtUsd and then rejects if any reserve can burn a share. Once collateral drops below the debt (after a 51% move from the 25% LTV entry point, instead of the 76% move used in the documented M-05 case) finalizeDust succeeds, _writeOff records 2 units of bad debt and sets frozen = true for the entire bank, which blocks all borrowing and all debt-bearing withdrawals until coverBadDebt plus two timelocked unfreeze calls. The cost to an attacker is three transactions per throwaway address, and each address can repeat the halt after every restart while the price stays below the threshold. docs/SECURITY_REVIEW.md M-05 records the global halt as an accepted residual; this finding sharpens it: the ceil rounding halves the price move needed and the intermediate band (collateral between 1.00x and 1.08x of a 1-2 unit debt) is unreachable by both liquidate and finalizeDust, so there is no way to clear such positions before they become losses.","line":593,"path":"src/IMDBank.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDBank} from \"src/IMDBank.sol\";\n\ncontract DustToken {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address t, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[t] += a;\n        return true;\n    }\n}\n\ncontract DustOracle {\n    mapping(address => uint256) public p;\n\n    function set(address a, uint256 v) external {\n        p[a] = v;\n    }\n\n    function price(address a) external view returns (uint256) {\n        require(p[a] != 0, \"no price\");\n        return p[a];\n    }\n}\n\n/// @notice A one-base-unit USDC debt ($0.000001) is admitted, its displayed debt doubles to two units after\n/// one second of accrual through ceil rounding, it is then neither liquidatable nor finalizable while the\n/// collateral still covers it, and after a 51% IMD move it is finalized as bad debt and freezes the entire bank.\ncontract DustPositionGlobalFreezeTest is Test {\n    address constant GUARDIAN = address(0xBEEF);\n    address constant ATTACKER = address(0xA77AC);\n    address constant LIQ = address(0xCAFE);\n\n    IMDBank bank;\n    DustOracle oracle;\n    DustToken imd;\n    DustToken usdc;\n    DustToken usdt;\n    DustToken weth;\n\n    function setUp() public {\n        vm.warp(100 days);\n        imd = new DustToken(18);\n        usdc = new DustToken(6);\n        usdt = new DustToken(6);\n        weth = new DustToken(18);\n        oracle = new DustOracle();\n        oracle.set(address(imd), 10e18);\n        oracle.set(address(usdc), 1e18);\n        oracle.set(address(usdt), 1e18);\n        oracle.set(address(weth), 2000e18);\n        bank = new IMDBank(\n            address(this), GUARDIAN, address(imd), address(oracle), address(usdc), address(usdt), address(weth)\n        );\n        bank.configureRisk(2500, 3500, 800, 5000, 1e30);\n        bank.configureReserve(address(usdc), 1e30, 0.02e27, 0.08e27, 0.9e27, 8000);\n        bank.setReserveFrozen(address(usdc), false);\n        bank.setFrozen(false);\n        usdc.mint(address(this), 1_000_000e6);\n        usdc.approve(address(bank), type(uint256).max);\n        bank.donateLiquidity(address(usdc), 1_000_000e6);\n        imd.mint(ATTACKER, 1e18);\n        usdc.mint(LIQ, 1e12);\n        vm.prank(ATTACKER);\n        imd.approve(address(bank), type(uint256).max);\n        vm.prank(LIQ);\n        usdc.approve(address(bank), type(uint256).max);\n    }\n\n    function test_oneUnitDebtCannotFreezeTheWholeBank() public {\n        vm.startPrank(ATTACKER);\n        bank.supply(4e11, ATTACKER); // 0.0000004 IMD = $0.000004 at $10\n        bank.setCollateralEnabled(true);\n        // Exactly at the 25% LTV: one USDC base unit ($0.000001). A minimum-debt guard would reject this.\n        try bank.borrow(address(usdc), 1, ATTACKER) {}\n        catch {\n            vm.stopPrank();\n            return;\n        }\n        vm.stopPrank();\n        assertEq(bank.previewDebt(ATTACKER, address(usdc)), 1);\n\n        // One second later the one-share debt is ceil(1 * (1e27 + ceil(0.02e27/365d)) / 1e27) = 2 units.\n        vm.warp(block.timestamp + 1);\n        bank.accrue(address(usdc));\n        assertEq(bank.previewDebt(ATTACKER, address(usdc)), 2, \"ceil rounding doubled a one-share debt\");\n        (,,,, uint256 hf) = bank.accountData(ATTACKER);\n        assertLt(hf, 1e18); // 4e12 * 0.35 / 2e12 = 0.70 without any price move\n\n        // A 51% move ($10 -> $4.90). Collateral $0.00000196 < debt $0.000002.\n        oracle.set(address(imd), 4.9e18);\n        // Ordinary liquidation is impossible: covered amount floors to 1 unit, which cannot burn one share.\n        vm.prank(LIQ);\n        vm.expectRevert(IMDBank.Dust.selector);\n        bank.liquidate(ATTACKER, address(usdc), type(uint256).max, 0, block.timestamp);\n\n        // Anyone can now write the position off. Two base units of loss must not halt all lending.\n        try bank.finalizeDust(ATTACKER) {} catch {}\n        assertFalse(bank.frozen(), \"a $0.000002 loss froze the entire bank\");\n    }\n}","reproduction":"State: LTV 2500, threshold 3500, bonus 800, USDC reserve at 2% base rate, IMD at $10. Attacker: supply(4e11, attacker) (= $0.000004), setCollateralEnabled(true), borrow(USDC, 1, attacker) (capacity floor(4e12*0.25) = 1e12 USD-wei = exactly 1 unit, accepted). previewDebt == 1. warp +1 second, accrue(USDC): previewDebt == 2 (expected: 1 unit plus 6.3e-10 of interest). accountData HF = 1.4e12*1e18/2e12 = 0.70e18. Now set IMD to $4.90: collateral 1.96e12 USD-wei < debt 2e12. liquidate(attacker, USDC, max, 0, now) reverts Dust() (0xb4801272): coveredAmount = floor(1.96e12/1.08/1e12) = 1, burned = floor(1e27/1.000000000634e27) = 0, paid = 0. finalizeDust(attacker) succeeds: ceil collateral 1.96e12 <= 1e15 and < 2e12, _requireUnliquidatableDust finds paid == 0, so badDebt = 2, reserve.frozen = true and bank.frozen() == true. Expected: a $0.000002 position cannot stop lending for every user. Intermediate stuck band shown separately: at IMD $2.60 with debt still 1 unit (collateral 1.04e12 vs debt 1e12, HF 0.364) liquidate reverts Dust() and finalizeDust reverts InvalidAmount() (0x2c5211c6), so the account is unreachable by either path until rounding pushes it into the loss branch. Suggested fixes that keep the agreed design: enforce a minimum debt value per account/asset in borrow() (for example debtUsd >= 1e18 after the mint) so one-share debts cannot exist, and/or let dust write-offs below a small USD amount record badDebt and freeze only the affected reserve instead of the global frozen flag.","severity":"medium","snippet":"        return Math.mulDiv(shares, index, RAY, Math.Rounding.Ceil);","title":"Sub-unit debt positions: one-share debt doubles after one second of accrual, sits in a band where neither liquidate nor finalizeDust can execute, then a 51% move turns $0.000002 into bad debt that fre"},{"citation":"resolved","description":"Boundary x precision. _liquidationQuote computes seized = floor(seizedValue * collateralUnit / collateralPrice). When one wei of IMD is worth more than paid * debtPrice * 1.08 (collateral price near the 1e27 MAX_PRICE bound with a few wei of collateral), seized rounds to 0 while paid > 0. The dust sweep that would hand over the whole residual requires seized != 0, so the quote falls through to 'if (paid == 0 || seized == 0) revert Dust()'. finalizeDust then rejects the same account because _requireUnliquidatableDust sees paid != 0 (a share is affordable). The account is unreachable by both paths. The precondition (IMD above roughly $2e8 per token) is only reachable at the accepted price boundary, so this is reported as a logic gap with a trivial fix rather than a live loss.","line":477,"path":"src/IMDBank.sol","reproduction":"Oracle IMD = 1e27 (MAX_PRICE), WETH = 2000e18. supply(1, alice) (1 wei IMD = 1e9 USD-wei), setCollateralEnabled(true), borrow(WETH, 125000, alice) (debt 2.5e8 USD-wei, capacity 2.5e8). Set IMD to 2e26: collateral 2e8, debt 2.5e8, HF 0.28. liquidate(alice, WETH, max, 0, now): coveredAmount = floor(floor(2e8*10000/10800)*1e18/2000e18) = 92592, paid = 92592, seizedValue = floor(92592*2000)*10800/10000 = 199998720, seized = floor(199998720*1e18/2e26) = 0, sweep skipped because seized == 0, reverts Dust() (0xb4801272). finalizeDust(alice): ceil collateral 2e8 <= 1e15 and < 2.5e8, but _requireUnliquidatableDust computes paid = 92592 != 0 and reverts InvalidAmount() (0x2c5211c6). Expected: with budget == coveredAmount and paid > 0 the sweep should set seized = available (1 wei) so the liquidator closes the position. Fix: drop the 'seized != 0' conjunct (the paid == 0 check after it still protects the liquidator).","severity":"low","snippet":"            quote.budget == quote.coveredAmount && seized != 0","title":"Liquidation dust sweep is skipped when seized floors to zero, leaving a position that neither liquidate nor finalizeDust can clear"},{"citation":"resolved","description":"Numerical gap (queryX vs doX). projectedHealth reproduces accountData's health factor (threshold-based, 35%), and app.js only marks the projection as dangerous when it is below 1.0. IMDBank._requireBorrowSafe (src/IMDBank.sol:536) rejects borrow and debt-bearing withdraw when debtUsd > borrowCapacityUsd, which uses ltvBps (25%). Every borrow or withdrawal that leaves the account between HF 1.0 and HF 1.4 (threshold/LTV) is shown as a non-danger projection and then fails with UnsafePosition at simulation. The review dialog's pre-checks (cash, supplied balance, wallet balance) do not include capacity either. No funds are at risk because the contract is the source of truth; the gap is that the displayed number cannot tell users the real rejection boundary.","line":68,"path":"web/core.js","reproduction":"Snapshot: collateralUsd 10000e18, debtUsd 0, threshold 3500, ltv 2500, IMD price 10e18, enabled true. Borrow form: 3000 USDC (amount 3000e6, decimals 6, price 1e18). projectedHealth returns 10000e18*3500/10000*1e18/3000e18 = 1.1666e18, rendered as 'Projected health factor: 1.166' without the danger class. Contract: capacity = 2500e18, debtUsd = 3000e18 > capacity, borrow reverts UnsafePosition. Same for withdraw: with debt 2500e18, withdrawing 1 IMD projects HF 1.3986 (safe) and reverts on chain. Fix: compute remaining capacity (ltvBps) in projectedHealth or in review() and flag/disable actions whose projected debt exceeds projected collateral * ltvBps / 10000.","severity":"low","snippet":"  return d === 0n ? MaxUint256 : c * liquidationBps / 10000n * USD / d;","title":"Frontend projected health factor omits the LTV capacity check the contract actually enforces on borrow and withdraw, so a projection shown as safe is rejected on chain"}],"hash":"ae81ceffbaf22d9d5ec325be07e27fd9e899a4cc0b322ee61e06c9552befb74c","nodeId":"2a82220d-dfe5-4dab-9186-8caafb09c1e4","outcome":"completed","summary":"Review complete. Findings are in `.imd-findings.json` (4 findings, 23 coverage rows, 2 Foundry proofs). No source files were changed; `git status` shows only the gitignored scratch tests and the findings file.\n\n**What I covered.** I traced the full math surface of `IMDBank`, `RiskOracle` and `GovernanceTimelock` against the Math Precision, Boundary and Numerical Gap guides: the per-second compounding index, share minting and burning, exact-reduction repayment, liquidation quoting and the dust sweep, health-factor and LTV checks, oracle normalisation and band logic, and the frontend's projected health factor. I also fuzzed liquidation value bounds, repayment exactness and borrow rounding at several thousand runs each. The core accounting held up: debt always rounds in the reserve's favour, partial repayment never charges more than the budget, seized collateral never exceeds paid value times the bonus plus the documented dust sweep, and checkpoint frequency does not change interest.\n\n**Findings, most severe first.**\n\n- **Medium, RiskOracle band blocks liquidation (proof attached).** Every feed must have a `minPrice`, and `price()` reverts when two agreeing, fresh feeds fall below it, on the collateral side too. In the proof, IMD configured with a $5 floor crashes to $4.00 and a borrower at health factor 0.56 cannot be liquidated or finalized; the only repair is a timelocked reconfiguration. The suggested fix keeps the band but hard-rejects only the direction that would overvalue the borrower.\n- **Medium, sub-unit debt positions freeze the whole bank (proof attached).** A one-base-unit USDC debt is admitted at exactly 25% LTV. After one second the ceil rounding on one share makes the displayed debt two units, dropping the health factor from 1.40 to 0.70 with no price move. There is then a band where neither liquidate nor finalizeDust can execute, and after a 51% move the account is finalized as two units of bad debt that set the global freeze. This sharpens the accepted M-05 residual: the rounding halves the price move needed. Suggested fix is a minimum debt value in borrow and/or a reserve-scoped freeze for dust losses.\n- **Low, liquidation sweep skipped when `seized` floors to zero.** With collateral near the price bound, `paid > 0` but `seized == 0`, so liquidate reverts Dust and finalizeDust rejects because a share is affordable. Precondition is only reachable near `MAX_PRICE`; the fix is dropping one conjunct.\n- **Low, frontend projection omits the LTV capacity check.** The projected health factor only flags values below 1.0, while the contract rejects borrows and withdrawals above 25% LTV, so projections between 1.0 and 1.4 look safe and revert on chain.\n\n**Static-analysis leads** on strict equality, reentrancy and the uninitialised `quote` local were traced and are benign under the shared reentrancy lock and the timelock's pre-call state writes. Not reached in depth: the Mainnet fork test and the browser transaction flow, which need network access.","treeHash":null,"usage":{"cachedInputTokens":2616438,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":68326,"runtime":"claude","turns":41,"wallClockMs":983044}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"e51446c90abcd5cc","findings":[],"hash":"c03ec47af119d135510d6b02ba316fa3ee8e6517f8cdffc77a2d6f82bc5a751f","nodeId":"6fdf6ae3-4c22-4332-9035-c99d5dfdaf8f","outcome":"failed","summary":"runtime reported <synthetic>, not the required premium model claude-fable-5-1","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"<synthetic>","outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":2748}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5278e52f0f1704f6","findings":[],"hash":"c2473d8ce3a9fc68224969365088434affa9e75b3874c86fe31844bbb2b0a2bd","nodeId":"6fdf6ae3-4c22-4332-9035-c99d5dfdaf8f","outcome":"failed","summary":"runtime reported <synthetic>, not the required premium model claude-fable-5-1","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"<synthetic>","outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":2382}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"87804e27e9c9f85a","findings":[{"citation":"resolved","description":"RiskOracle.configure rejects minPrice == 0 (line 68), and RiskOracle.price reverts InvalidPrice whenever low < f.minPrice (line 108) regardless of collateralSide. For the collateral feed a lower price is always the conservative direction for the bank, so the floor adds no solvency protection; it only removes liquidation liveness exactly when it is needed. ARCHITECTURE.md tells governance to set 'much narrower asset-specific bands' than the bank's 1e27 bound, so in production IMD will carry a floor a few multiples below its entry price. When both independent feeds agree on a price below that floor (a real crash, not a feed fault, since the deviation check still passes), IMDBank._price reverts and with it accountData, liquidate, finalizeDust, borrow and debt-bearing withdraw. Repay remains possible but insolvent borrowers have no incentive to repay. Interest keeps accruing and bad debt grows for the whole outage. The guardian cannot help (setEnabled(false) only makes it worse; configure is governor-only), and the governor is the GovernanceTimelock with an immutable >= 2 day delay, after which RiskOracle.configure additionally re-validates price() at execution time and reverts again if the market has moved outside the new band. Victim: reserve donors / protocol solvency (requester priority: solvency > oracle security > liquidation reliability). Fix options that preserve the design: allow minPrice == 0 to mean 'no floor' for collateralSide feeds (the deviation and max bands still apply; low >= 1 is guaranteed by _read so the mulDiv denominator is safe), or treat a sub-floor collateral price as valid for liquidation/finalizeDust while still blocking borrow/withdraw. The attached proof assumes the first option.","line":68,"path":"src/RiskOracle.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDBank} from \"src/IMDBank.sol\";\nimport {RiskOracle} from \"src/RiskOracle.sol\";\n\ncontract ProofToken {\n    uint8 public immutable decimals;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint8 d) {\n        decimals = d;\n    }\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n\n    function transferFrom(address f, address t, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        balanceOf[f] -= a;\n        balanceOf[t] += a;\n        return true;\n    }\n}\n\ncontract ProofFeed {\n    uint8 public decimals = 8;\n    int256 public answer;\n\n    constructor(int256 a) {\n        answer = a;\n    }\n\n    function set(int256 a) external {\n        answer = a;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, answer, block.timestamp, block.timestamp, 1);\n    }\n}\n\n/// @notice Fails on current code: governance cannot express \"no lower price band\" for the collateral\n/// feed (minPrice == 0 is rejected), so a genuine crash through any configured floor makes every\n/// price-dependent path revert, including liquidation of a position that is already insolvent.\n/// Passes once RiskOracle.configure accepts minPrice == 0 (no floor) for collateral-side feeds.\ncontract OracleFloorProofTest is Test {\n    ProofToken imd;\n    ProofToken usdc;\n    ProofToken usdt;\n    ProofToken weth;\n    RiskOracle oracle;\n    IMDBank bank;\n    ProofFeed imdA;\n    ProofFeed imdB;\n    address constant GUARDIAN = address(0xBEEF);\n    address constant ALICE = address(0xA11CE);\n    address constant LIQ = address(0xCAFE);\n\n    function setUp() public {\n        vm.warp(100 days);\n        imd = new ProofToken(18);\n        usdc = new ProofToken(6);\n        usdt = new ProofToken(6);\n        weth = new ProofToken(18);\n        oracle = new RiskOracle(address(this), GUARDIAN);\n        imdA = new ProofFeed(10e8);\n        imdB = new ProofFeed(10e8);\n        ProofFeed usdcA = new ProofFeed(1e8);\n        ProofFeed usdcB = new ProofFeed(1e8);\n        ProofFeed usdtA = new ProofFeed(1e8);\n        ProofFeed usdtB = new ProofFeed(1e8);\n        ProofFeed wethA = new ProofFeed(2000e8);\n        ProofFeed wethB = new ProofFeed(2000e8);\n        oracle.configure(address(usdc), address(usdcA), address(usdcB), 1 days, 1 days, 200, 0.5e18, 2e18, false);\n        oracle.configure(address(usdt), address(usdtA), address(usdtB), 1 days, 1 days, 200, 0.5e18, 2e18, false);\n        oracle.configure(\n            address(weth), address(wethA), address(wethB), 1 hours, 1 hours, 500, 100e18, 100_000e18, false\n        );\n        bank = new IMDBank(\n            address(this), GUARDIAN, address(imd), address(oracle), address(usdc), address(usdt), address(weth)\n        );\n        bank.configureRisk(2500, 3500, 800, 5000, 1_000_000e18);\n        bank.configureReserve(address(usdc), 1_000_000e6, 0.02e27, 0.08e27, 0.9e27, 8000);\n        bank.setFrozen(false);\n        usdc.mint(address(this), 1_000_000e6);\n        usdc.approve(address(bank), type(uint256).max);\n        bank.donateLiquidity(address(usdc), 1_000_000e6);\n        imd.mint(ALICE, 1000e18);\n        usdc.mint(LIQ, 1_000_000e6);\n        vm.prank(LIQ);\n        usdc.approve(address(bank), type(uint256).max);\n    }\n\n    function test_collateralFeedWithoutFloorKeepsLiquidationAliveInCrash() public {\n        // Governance wants no lower band on the collateral: a lower collateral price is always\n        // the conservative direction for the bank, and the two-source agreement check still applies.\n        oracle.configure(address(imd), address(imdA), address(imdB), 1 hours, 1 hours, 500, 0, 100e18, true);\n\n        vm.startPrank(ALICE);\n        imd.approve(address(bank), type(uint256).max);\n        bank.supply(1000e18, ALICE);\n        bank.setCollateralEnabled(true);\n        bank.borrow(address(usdc), 2500e6, ALICE);\n        vm.stopPrank();\n\n        // Genuine crash: both independent sources agree at $1.90. Collateral $1,900 < debt $2,500.\n        imdA.set(1.9e8);\n        imdB.set(1.9e8);\n        (, uint256 debtUsd,,, uint256 hf) = bank.accountData(ALICE);\n        assertEq(debtUsd, 2500e18);\n        assertLt(hf, 1e18);\n\n        // Liquidation must remain possible so the loss is realized while collateral still has value.\n        vm.prank(LIQ);\n        (uint256 repaid, uint256 seized) =\n            bank.liquidate(ALICE, address(usdc), type(uint256).max, 0, block.timestamp);\n        assertGt(repaid, 0);\n        assertEq(seized, 1000e18);\n        (,,,,, uint256 badDebt,) = bank.reserveData(address(usdc));\n        assertEq(badDebt, 2500e6 - repaid);\n    }\n}","reproduction":"Setup (test/scratch/Econ.t.sol test_floorBlocksLiquidationInCrash): RiskOracle configured for IMD with two agreeing feeds at $10, minPrice = 2e18, maxPrice = 100e18, collateralSide = true; bank at LTV 25 / threshold 35 / bonus 8; Alice supplies 1000 IMD and borrows 2500 USDC. Both IMD feeds move to $1.90 (within deviation, below floor). Expected: position is insolvent (collateral $1,900 < debt $2,500), liquidate(ALICE, USDC, max, 0, now) should repay ~1759.26 USDC and seize all 1000 IMD, recording ~740 USDC bad debt. Actual: accountData, liquidate and finalizeDust all revert RiskOracle.InvalidPrice; guardian call to configure reverts Unauthorized; configure(minPrice = 0) reverts InvalidConfiguration, so there is no way to express 'no floor'. At $2.10 (just above the floor) the same position already shows HF 0.294, i.e. the floor sits well inside the insolvent region.","severity":"medium","snippet":"                || maxDeviationBps > 2000 || minPrice == 0 || maxPrice <= minPrice || maxPrice > 1e36","title":"Collateral-side oracle floor is mandatory and reverts, so a genuine IMD crash through the configured floor blocks all liquidations and dust finalization until a 2-day timelock reconfiguration"},{"citation":"resolved","description":"supply() enforces one shared cap on all deposits, including top-ups by accounts that already carry debt. A depositor with no debt pays no fee, earns nothing, and can withdraw at any time: debt-free withdraw skips the oracle, the freeze and the health check (withdraw line 194 only checks when _hasDebt). So a griefer can hold exactly the remaining headroom (supplyCap - totalCollateral) in the bank during a volatile period at no cost beyond gas, and every borrower who tries to defend a falling health factor by adding IMD gets CapExceeded. The borrower's only remaining defence is repaying with the debt token, which they may not hold. The griefer (or anyone) then liquidates for the 8% bonus and withdraws the filler IMD immediately, even while the bank is frozen or the oracle is down. The tighter governance sets the cap (the docs say production caps should be 'orders of magnitude smaller'), the cheaper the attack: with a 20,000 IMD cap and 19,000 IMD supplied, 1,000 IMD (~$10k at $10) locks out all top-ups. ARCHITECTURE.md notes 'governance must retain rescue headroom', but the headroom itself is what the griefer fills, and the governor is a >= 2-day timelock so the cap cannot be raised in time. Fix that preserves the cap as a risk control: let an account that already has debt (or whose HF is below some bound) add collateral past the cap, since a top-up only reduces protocol risk; or exclude debt-free balances from the cap and cap only enabled collateral backing debt.","line":175,"path":"src/IMDBank.sol","reproduction":"test/scratch/Econ.t.sol test_supplyCapFillBlocksTopUp: cap 1,000,000 IMD, Alice 1000 IMD / 2500 USDC debt. Griefer supplies room = supplyCap - totalCollateral (999,000 IMD), no debt. IMD feeds move $10 -> $7 (Alice HF 0.98). Alice calls supply(1, ALICE) with 500 IMD in hand: expected success (top-up lowers risk), actual revert CapExceeded. Liquidator then liquidates Alice: paid 1,250 USDC, seized 192.857 IMD (= $1,350 at $7, an 8% premium of $100 Alice could have avoided). Griefer calls withdraw(room, griefer) and gets all 999,000 IMD back; net cost zero.","severity":"medium","snippet":"        if (amount > supplyCap || totalCollateral > supplyCap - amount) revert CapExceeded();","title":"Supply cap can be filled at zero cost by a debt-free depositor, blocking every borrower's collateral top-up ahead of a liquidation the filler can then execute"},{"citation":"resolved","description":"configure rejects any maxAge above 1 day and the bound is immutable. The canonical Chainlink USDC/USD and USDT/USD mainnet feeds (the research's own evidence) have an 86,400 s heartbeat and 0.25% deviation; in calm markets a new round lands at heartbeat + transmission latency, i.e. a few seconds to minutes after 86,400 s. _read reverts when block.timestamp - updated > maxAge (line 120), so with the only admissible setting (86400) every day there is a window of that latency during which price(USDC) / price(USDT) revert. Because accountData reads the price of every asset the account owes, any account with USDC or USDT debt cannot be liquidated, borrow any asset, withdraw collateral, or be dust-finalized during the window, while interest accrues. If a stablecoin crash or an IMD crash coincides with the window, liquidators lose that time. DEPLOYMENT.md acknowledges the tradeoff but the contract gives governance no way to resolve it. Fix: allow a modest tolerance above 1 day (e.g. up to 1 day + 1 hour, or a per-feed bound up to 2 days) so operators can set heartbeat + buffer, which is standard Chainlink integration guidance.","line":67,"path":"src/RiskOracle.sol","reproduction":"test/scratch/Econ.t.sol test_maxAgeHardBoundVsHeartbeat: USDC feeds configured with primaryMaxAge = secondaryMaxAge = 1 days; both USDC rounds updated at t0. At t0 + 1 day accountData(ALICE) succeeds. At t0 + 1 day + 60 s (next round not yet landed) accountData reverts InvalidPrice; IMD feeds drop to $5 (HF 0.7) and liquidate(ALICE, USDC, max, 0, now) reverts InvalidPrice instead of liquidating. configure(..., 1 days + 1, ...) reverts InvalidConfiguration, so no buffer can be configured.","severity":"low","snippet":"                || primaryMaxAge > 1 days || secondaryMaxAge > 1 days || maxDeviationBps == 0","title":"Hard 1-day maximum feed age equals Chainlink's 24h heartbeat for USDC/USD and USDT/USD, creating a recurring daily window in which stablecoin-debt positions cannot be liquidated, borrowed against or w"},{"citation":"resolved","description":"BankHandler only borrows, repays and liquidates USDC; it never calls borrow/liquidate on USDT or WETH, never calls finalizeDust or coverBadDebt, never changes risk or reserve parameters, and keeps utilization far below the kink (1,000,000 USDC cash versus <= 10,000 USDC borrows), so the slope2 branch of _updateRate and the MAX_INDEX saturation path are never reached under random sequences. invariant_cashFlowConservation and invariant_collateralConservation therefore say nothing about the multi-reserve _writeOff loop (which burns debt in reserves the handler never touched), about repeated collateral-limited liquidations converging to zero, or about the dust rule leaving a residue above $0.001 after a WETH-denominated liquidation. Concrete untested scenarios: (a) account with USDC+USDT+WETH debt liquidated first on WETH then on USDC with hf < 0.95 and collateral between 1.08 x D_usdc and 1.08 x D_total; (b) finalizeDust while another account holds healthy multi-asset debt, then coverBadDebt partially and attempt setFrozen(false); (c) borrow pushing utilization above the kink then 365 days of accrual under the 100% ceiling. These are the paths my manual trace says hold, but the suite does not demonstrate it.","line":45,"path":"test/Invariants.t.sol","reproduction":"Run forge test --match-contract BankInvariantTest -vvv and inspect the selector table: only usdc paths are called; grep test/ for finalizeDust/coverBadDebt shows only fixed unit tests (Audit.t.sol, RiskControls.t.sol), no randomized sequencing. Expected: a handler that selects the reserve from the fuzz input and includes finalizeDust/coverBadDebt/configureRisk/configureReserve, with invariants totalDebtShares == sum of user shares per reserve, badDebt != 0 => reserve frozen && bank frozen, and sum(collateralBalance) == totalCollateral across all actors.","severity":"info","snippet":"        try bank.borrow(address(usdc), amount, actor) {","title":"Stateful invariant and fuzz suites never exercise multi-asset debt, finalizeDust, coverBadDebt, reserve/risk reconfiguration or high-utilization rates, so the conservation claims are only established "}],"hash":"e0e3d0a3c4a3950b29e2c8883a86c0616700becff330c9883478f9f01c3d6e88","nodeId":"f91b7e43-3fe9-4ce7-a2f4-af40e9f4938e","outcome":"completed","summary":"The review is complete. The findings are in `.imd-findings.json` at the repository root; no source files were changed (the findings file and `test/scratch/` are gitignored, so the tree is clean).\n\n**What I audited.** The Economic Security, Invariant and Flow Gap passes over `IMDBank.sol`, `RiskOracle.sol`, `GovernanceTimelock.sol` and `ExactToken.sol`, plus the test suites. I traced the indexed debt accounting, every rounding direction, the liquidation quote and dust rule, the write-off and recapitalization cycle, the rate model, the oracle validation and the timelock. The core accounting holds: share minting and burning stay paired, partial repayments telescope so no rounding farm exists, the collateral-limited liquidation path is reachable only when the position is already insolvent at bonus-adjusted terms, repeated liquidations converge, and the documented M-05 dust-halt needs a genuine 73%+ IMD drop rather than anything an attacker controls.\n\n**Findings reported** (4, none critical or high):\n\n- **Medium, oracle floor blocks liquidation in a crash.** `RiskOracle.configure` forces a nonzero `minPrice` and `price()` reverts below it even for the collateral feed, where a low price is the safe direction. Two agreeing feeds at $1.90 under a $2 floor make `liquidate`, `finalizeDust`, `accountData` all revert on an already insolvent position, and only a 2-day timelock can fix it. Includes a Foundry proof (`test/scratch/OracleFloorProof.t.sol`) that fails now and passes with a one-line fix allowing a zero floor on collateral-side feeds, verified in a throwaway copy.\n- **Medium, zero-cost supply-cap fill.** A debt-free depositor can hold the remaining cap headroom, so borrowers get `CapExceeded` on top-ups during a dip, get liquidated for the 8% bonus, and the filler withdraws immediately with no fee, freeze or oracle dependency.\n- **Low, 1-day maximum age equals the stablecoin heartbeat.** With Chainlink USDC/USD and USDT/USD at 86,400 s and the bound immutable, every daily round latency is a window where stablecoin-debt accounts cannot be liquidated, borrow or withdraw.\n- **Info, test gaps.** The invariant handler only exercises USDC paths and never calls `finalizeDust`, `coverBadDebt`, reconfiguration or above-kink rates.\n\n**Coverage.** All 20 verifier entry points have a row (16 hold, 4 reference findings), plus 4 invariant rows. Nothing was left unreached.","treeHash":null,"usage":{"cachedInputTokens":1685061,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":71817,"runtime":"claude","turns":38,"wallClockMs":990808}}],"verification":[{"checks":[{"durationMs":4632,"exitCode":0,"name":"build","output":"Compiling 42 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.47s\nCompiler run successful!\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:88:9\n   │\n88 │         emit FeedConfigured(asset, primary, secondary, collateralSide);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:89:9\n   │\n89 │         emit FeedEnabled(asset, true);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:97:9\n   │\n97 │         emit FeedEnabled(asset, enabled);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:119:78\n    │\n119 │             answer <= 0 || round == 0 || answered < round || updated == 0 || updated > block.timestamp\n    │                                                                              ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:120:57\n    │\n120 │                 || started == 0 || started > updated || block.timestamp - updated > maxAge\n    │                                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:123:13\n    │\n123 │         if (uint256(answer) > 1e36 / 10 ** (18 - decimals_)) revert InvalidPrice();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:124:16\n    │\n124 │         return uint256(answer) * 10 ** (18 - decimals_);\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `executing` is updated\n   ╭▸ src/GovernanceTimelock.sol:70:44\n   │\n70 │         (bool ok, bytes memory returned) = target.call(data);\n   │                                            ━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:62:51\n   │\n62 │             executing || done[id] || when == 0 || block.timestamp < when\n   │                                                   ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:63:20\n   │\n63 │                 || block.timestamp > when + GRACE_PERIOD\n   │                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/GovernanceTimelock.sol:55:22\n   │\n55 │     function execute(address target, bytes calldata data, bytes32 salt)\n   │                      ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/GovernanceTimelock.sol:73:9\n   │\n73 │         emit Executed(id);\n   │         ━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:22:26\n   │\n22 │         uint8 decimals = IExactERC20(token).decimals();\n   │                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:134:9\n    │\n134 │         address collateral_,\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:135:9\n    │\n135 │         address oracle_,\n    │         ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:154:64\n    │\n154 │             if (asset == collateral_ || assetUnit[asset] != 0) revert InvalidConfiguration();\n    │                                                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:21:37\n   │\n21 │         if (token.code.length == 0) revert InvalidToken();\n   │                                     ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:23:28\n   │\n23 │         if (decimals > 18) revert InvalidToken();\n   │                            ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:200:9\n    │\n200 │         emit Withdrawn(msg.sender, to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:218:9\n    │\n218 │         emit Borrowed(msg.sender, asset, to, amount, shares);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:237:9\n    │\n237 │         emit Repaid(msg.sender, onBehalfOf, asset, paid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:246:9\n    │\n246 │         emit LiquidityDonated(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:257:9\n    │\n257 │         emit BadDebtCovered(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:272:13\n    │\n272 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:512:13\n    │\n512 │             emit BadDebtRecorded(account, asset, amount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:513:13\n    │\n513 │             emit FrozenStateChanged(asset, true);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:514:13\n    │\n514 │             emit FrozenStateChanged(address(0), true);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:286:9\n    │\n286 │         emit Liquidated(msg.sender, account, asset, repaid, seized);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:308:9\n    │\n308 │         emit DustFinalized(msg.sender, account, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMDBank.sol:597:18\n    │\n597 │         price_ = IBankOracle(oracle).price(token);\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:602:36\n    │\n602 │         if (assetUnit[asset] == 0) revert UnsupportedAsset();\n    │                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:562:13\n    │\n562 │             revert InvalidConfiguration();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:598:48\n    │\n598 │         if (price_ == 0 || price_ > MAX_PRICE) revert InvalidPrice();\n    │                                                ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:416:9\n    │\n416 │         emit ReserveConfigured(asset, borrowCap, baseRateRay, slope1Ray, slope2Ray, kinkBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:423:55\n    │\n423 │                 if (reserves[assets[i]].badDebt != 0) revert OutstandingBadDebt();\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:495:28\n    │\n495 │             if (paid != 0) revert InvalidAmount();\n    │                            ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:17:16\n   │\n17 │         return IExactERC20(token).balanceOf(account);\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:555:9\n    │\n555 │         emit Accrued(asset, reserve.index, reserve.cachedRateRay);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:559:45\n    │\n559 │         if (reserve.totalDebtShares == 0 || block.timestamp == reserve.lastAccrual) return reserve.index;\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:561:13\n    │\n561 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:561:51\n    │\n561 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:569:16\n    │\n569 │         while (elapsed != 0) {\n    │                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:570:17\n    │\n570 │             if (elapsed & 1 != 0) accumulated = _rayMulCapped(accumulated, factor);\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:572:17\n    │\n572 │             if (elapsed != 0) factor = _rayMulCapped(factor, factor);\n    │                 ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:51:17\n   │\n51 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:61:17\n   │\n61 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/RiskControls.t.sol:61:21\n   │\n61 │         uint256 t = block.timestamp;\n   │                     ━━━━━━━━━━━━━━━\n62 │         vm.warp(t + 30 days);\n   │         ──────────────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":9429,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/OptionalMainnet.t.sol:OptionalMainnetTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 480.18µs (0.00ns CPU time)\n\nRan 8 tests for test/GovernanceEdges.t.sol:TimelockEdgesTest\n[PASS] test_cancelledProposalCannotUseOriginalMaturityAfterReschedule() (gas: 290640)\n[PASS] test_constructorRejectsRoleCollisionsAndDelayOutOfBounds() (gas: 3968)\n[PASS] test_domainBindsExecutorTargetCalldataAndSalt() (gas: 790352)\n[PASS] test_duplicateScheduleMalformedTargetsAndUnknownCancellation() (gas: 174487)\n[PASS] test_exactDelayAndGracePeriodEndpoints() (gas: 360909)\n[PASS] test_expiredProposalRequiresCancellationAndFreshDelay() (gas: 277357)\n[PASS] test_failedTargetRetainsProposalAndCanRetryWithoutRescheduling() (gas: 359028)\n[PASS] test_nestedExecutionCannotConsumeAnotherMaturedProposal() (gas: 988353)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 974.16µs (2.54ms CPU time)\n\nRan 2 tests for test/DeploymentRehearsal.t.sol:DeploymentRehearsalTest\n[PASS] test_explicitRolesFromFactoryLikeCallerAndTimelockedRisk() (gas: 11710046)\n[PASS] test_wrongChainAndEOARolesRejected() (gas: 5110289)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.16ms (1.26ms CPU time)\n\nRan 7 tests for test/RiskControls.t.sol:RiskControlsTest\n[PASS] test_capReductionCannotBlockRepayment() (gas: 676247)\n[PASS] test_hardRiskBoundsCannotBeBypassedByGovernor() (gas: 154047)\n[PASS] test_liquidityExhaustionRevertsWithoutMintingDebt() (gas: 318099)\n[PASS] test_lossRecognitionRecapitalizationAndRestart() (gas: 990244)\n[PASS] test_multiReserveDebtWrittenOffAfterCollateralExhausted() (gas: 1299263)\n[PASS] test_permissionlessLiquidationFitsGasBudget() (gas: 605718)\n[PASS] test_tokenBalanceDonationCannotRetroactivelyChangeRate() (gas: 491413)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 5.03ms (3.11ms CPU time)\n\nRan 4 tests for test/TokenBoundaryEdges.t.sol:TokenBoundaryEdgesTest\n[PASS] test_everyMutationRejectsCallbackReentryWithTheActualGuard() (gas: 3540098)\n[PASS] test_malformedAndInexactCollateralTransfersRollBackBothDirections() (gas: 3512899)\n[PASS] test_malformedAndInexactDebtTransfersCannotCreateOrEraseDebt() (gas: 5110373)\n[PASS] test_reentryGuardAlsoCoversOutgoingDebtRepaymentAndWithdrawal() (gas: 1363028)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 42.73ms (8.00ms CPU time)\n\nRan 4 tests for test/GovernanceTimelock.t.sol:GovernanceTimelockTest\n[PASS] test_cancellationAndAccess() (gas: 210103)\n[PASS] test_delayPermissionlessExecutionAndReplay() (gas: 266474)\n[PASS] test_domainSeparation() (gas: 22790)\n[PASS] test_expiryAndFailureAtomicity() (gas: 200959)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 304.50ms (5.08ms CPU time)\n\nRan 2 tests for test/GovernanceEdges.t.sol:BankAuthorityEdgesTest\n[PASS] testFuzz_unprivilegedAccountCannotChangeAnyRiskControl(address) (runs: 1000, μ: 232135, ~: 232221)\n[PASS] test_guardianRotationRevokesOldAuthorityAndReserveFreezeIsIsolated() (gas: 718096)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 340.69ms (339.59ms CPU time)\n\nRan 9 tests for test/Audit.t.sol:IndependentAuditTest\n[PASS] testAudit_actualTinyLossTriggersGlobalSafetyHalt() (gas: 771740)\n[PASS] testAudit_checkpointFrequencyCannotMateriallyChangeBorrowerDebt() (gas: 22765073)\n[PASS] testAudit_collateralCallbackCannotLiquidateOrBorrow() (gas: 328890)\n[PASS] testAudit_dustFinalizationRejectsHealthyValuableAndInvalidPrice() (gas: 421454)\n[PASS] testAudit_dustTransferFailureRollsBackWriteoff() (gas: 397286)\n[PASS] testAudit_extremeCollapseRecordsBadDebt() (gas: 332121)\n[PASS] testAudit_insolventButPartlyRecoverableDustMustLiquidateFirst() (gas: 254035)\n[PASS] testAudit_oracleConfigurationRollsBackIfBroken() (gas: 1029291)\n[PASS] testAudit_smallRecoverablePositionCannotTriggerGlobalFreeze() (gas: 970961)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 342.55ms (30.99ms CPU time)\n\nRan 17 tests for test/IMDBank.t.sol:IMDBankTest\n[PASS] testFuzz_borrowRepayRoundingCannotCreateAssets(uint64,uint32) (runs: 512, μ: 566639, ~: 565511)\nLogs:\n  Bound result 1374567346\n  Bound result 828977\n\n[PASS] testFuzz_roundTripNoCollateralGain(uint96) (runs: 512, μ: 222899, ~: 222738)\nLogs:\n  Bound result 26925122867991457815\n\n[PASS] test_completeLifecycleThreeReserves() (gas: 1429741)\n[PASS] test_flashSupplyBorrowWithdrawCannotEscapeDebt() (gas: 482815)\n[PASS] test_guardianCannotUnfreezeOrChangeRisk() (gas: 758192)\n[PASS] test_initialDeploymentCannotTakeRisk() (gas: 125602)\n[PASS] test_liquidationSlippageAndExpiryProtectPayer() (gas: 574550)\n[PASS] test_noReturnTokensWorkAndFalseReturnRejected() (gas: 620428)\n[PASS] test_oracleOutageStillAllowsRepaymentAndDebtFreeExit() (gas: 764952)\n[PASS] test_partialRepayNeverReducesDebtMoreThanPayment() (gas: 567950)\n[PASS] test_priceDropLiquidationAndCloseFactor() (gas: 867321)\n[PASS] test_reentrancyTransferCallbackCannotMintUnbackedReceipts() (gas: 313832)\n[PASS] test_rejectsOverborrowWithdrawalAndCollateralDisable() (gas: 740270)\n[PASS] test_safeHealthRejectsLiquidation() (gas: 488508)\n[PASS] test_stablecoinDepegAndWethSpikeChangeDebtValue() (gas: 780072)\n[PASS] test_supplyOnBehalfDoesNotEnableOthersCollateral() (gas: 173849)\n[PASS] test_taxedTransfersFailAtomicallyInAndOut() (gas: 772536)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 361.38ms (708.13ms CPU time)\n\nRan 16 tests for test/AccountingEdges.t.sol:AccountingEdgesTest\n[PASS] testFuzz_partialRepaymentMatchesCashAndDebtAcrossDecimals(uint8,uint32,uint256) (runs: 1000, μ: 767380, ~: 768069)\nLogs:\n  Bound result 744\n  Bound result 136348154\n\n[PASS] testFuzz_repeatedTinyBorrowRepayCyclesCannotExtractCash(uint8,uint32) (runs: 1000, μ: 4997493, ~: 4997072)\nLogs:\n  Bound result 1591125\n\n[PASS] test_borrowCapCountsOtherBorrowersAndAccruedInterest() (gas: 1230819)\n[PASS] test_exactBorrowCapacityAndWithdrawalBoundary() (gas: 990871)\n[PASS] test_exactHealthAndCloseFactorBoundaries() (gas: 984971)\n[PASS] test_failedRepaymentDoesNotBurnSharesOrConsumeAllowance() (gas: 607861)\n[PASS] test_interestUpdatesProspectivelyAtGovernanceRateChange() (gas: 765368)\n[PASS] test_kinkAndFullUtilizationRatesThenIdleIndex() (gas: 1047450)\n[PASS] test_liquidationTransferFailureRestoresAllThreeReservesAndWriteoffs() (gas: 1566551)\n[PASS] test_partialRecapitalizationCannotReopenAnyRisk() (gas: 1160043)\n[PASS] test_repayThirdPartyChargesOnlyDebtAndDoesNotTransferOwnership() (gas: 580407)\n[PASS] test_saturatedIndexStopsNewDebtButAllowsFullRepayAndExit() (gas: 911203)\n[PASS] test_supplyCapCountsAllAccountsAndDirectDonationsMintNoClaims() (gas: 500161)\n[PASS] test_unsupportedReserveCannotReachAnyDebtPath() (gas: 210313)\n[PASS] test_zeroAndBankRecipientsAreRejectedOnEveryUserPath() (gas: 642504)\n[PASS] test_zeroAndOversizedInputsFailBeforeMovingFunds() (gas: 508122)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 361.61ms (476.59ms CPU time)\n\nRan 7 tests for test/RiskOracle.t.sol:RiskOracleTest\n[PASS] testFuzz_agreementAlwaysUsesSafeSide(uint64,uint64) (runs: 512, μ: 334600, ~: 334401)\nLogs:\n  Bound result 299999999\n  Bound result 2000\n\n[PASS] test_ageBoundaryAndCircuitBreaker() (gas: 271330)\n[PASS] test_conservativeDirectionAndDecimals() (gas: 325304)\n[PASS] test_depegIsPricedNotAssumedOneDollar() (gas: 194230)\n[PASS] test_feedDecimalsCannotSilentlyChange() (gas: 66234)\n[PASS] test_governanceAndGuardianSeparation() (gas: 251828)\n[PASS] test_rejectsBadRoundsAndTimes() (gas: 470682)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 361.56ms (362.82ms CPU time)\n\nRan 11 tests for test/OracleIntegrationEdges.t.sol:OracleIntegrationEdgesTest\n[PASS] testFuzz_decimalNormalizationAcrossFullSupportedRange(uint8,uint8,uint16) (runs: 1000, μ: 278485, ~: 280115)\nLogs:\n  Bound result 0\n  Bound result 0\n  Bound result 450\n\n[PASS] testFuzz_deviationLimitAcceptsEqualityButRejectsOneWeiMore(uint16) (runs: 1000, μ: 358376, ~: 358114)\nLogs:\n  Bound result 11\n\n[PASS] test_bankRejectsStaleCollateralButAllowsOracleIndependentRescue() (gas: 1161927)\n[PASS] test_brokenFeedCannotBeReenabledAndFailedEnableRemainsDisabled() (gas: 317268)\n[PASS] test_eachSourceEnforcesItsOwnHeartbeatInclusively() (gas: 347152)\n[PASS] test_extremeAnswerRejectedBeforeNormalizationCanOverflow() (gas: 373849)\n[PASS] test_failedFeedReplacementPreservesOriginalPairAndConfiguration() (gas: 556749)\n[PASS] test_invalidExistingDebtFeedBlocksCrossReserveBorrowAndWithdrawal() (gas: 1304457)\n[PASS] test_oracleAgreementDoesNotAssumeStablecoinPegAndCanTriggerLiquidation() (gas: 1036210)\n[PASS] test_priceBandsRejectAgreementOutsideConfiguredRange() (gas: 502252)\n[PASS] test_zeroRoundMissingStartAndStartAfterUpdateFailOnBothSources() (gas: 768347)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 361.56ms (695.56ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BankInvariantTest\n[PASS]\nBankInvariantTest invariants:\n[PASS] invariant_cashFlowConservation\n[PASS] invariant_collateralConservation\n[PASS] invariant_debtSharesAndAggregateRounding\n BankInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------+-------------------+-------+---------+----------╮\n| Contract    | Selector          | Calls | Reverts | Discards |\n+==============================================================+\n| BankHandler | borrow            | 1174  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | donate            | 1182  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | elapse            | 1141  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | repay             | 1156  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | shockAndLiquidate | 1152  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | supply            | 1189  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | withdraw          | 1198  | 0       | 0        |\n╰-------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 75786329365\n  Bound result 11\n  Bound result 200000000000\n  Bound result 95349038758\n  Bound result 2514264337593543950335\n  Bound result 40000000\n  Bound result 1500\n  Bound result 822\n  Bound result 200\n  Bound result 8640000\n  Bound result 101\n  Bound result 2\n  Bound result 316\n  Bound result 69\n  Bound result 1500000000\n  Bound result 3000000000\n  Bound result 4230\n  Bound result 1\n  Bound result 100\n  Bound result 100000000\n  Bound result 262951952835853\n  Bound result 3600\n  Bound result 965\n  Bound result 365\n  Bound result 8094\n  Bound result 709551612\n  Bound result 6582\n  Bound result 4686\n  Bound result 500000000\n  Bound result 5248\n  Bound result 372946927413946\n  Bound result 1\n  Bound result 100000000000000000\n  Bound result 6472\n  Bound result 31536000\n  Bound result 1\n  Bound result 991577\n  Bound result 100000000000000000\n  Bound result 2514264337593543950335\n  Bound result 15\n  Bound result 800\n  Bound result 1191\n  Bound result 365\n  Bound result 18\n  Bound result 10343\n  Bound result 310713\n  Bound result 814986\n  Bound result 16\n  Bound result 10551\n  Bound result 788\n  Bound result 672609360\n  Bound result 18\n  Bound result 864000\n  Bound result 100000000000000000\n  Bound result 10612\n  Bound result 200000000\n  Bound result 100000000000000000\n  Bound result 424760645\n  Bound result 256268\n  Bound result 4001807748356\n  Bound result 100000000\n  Bound result 500\n  Bound result 605\n  Bound result 864000\n  Bound result 8644010535\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.43s (1.43s CPU time)\n\nRan 2 tests for test/ThreeReserveInvariants.t.sol:ThreeReserveInvariantTest\n[PASS] invariant_allReserveCashClaimsLossesAndRoundingAreAccountedFor() (runs: 256, calls: 24576, reverts: 0)\n\n╭---------------------+--------------+-------+---------+----------╮\n| Contract            | Selector     | Calls | Reverts | Discards |\n+=================================================================+\n| ThreeReserveHandler | borrow       | 2167  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | donate       | 2272  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | elapse       | 2254  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | finalizeDust | 2178  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | liquidate    | 2260  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | recovery     | 2249  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | repay        | 2267  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | shock        | 2194  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | supply       | 2305  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | toggle       | 2158  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | withdraw     | 2272  | 0       | 0        |\n╰---------------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 43950335\n  Bound result 100000000\n  Bound result 1500000000100000001\n  Bound result 10602\n  Bound result 343958713\n  Bound result 836\n  Bound result 18287\n  Bound result 1500000000000019127\n  Bound result 11516\n  Bound result 65492503066193772950\n  Bound result 1290\n  Bound result 514264337593543950335\n  Bound result 2595\n  Bound result 19617\n  Bound result 88466425\n  Bound result 30270473\n  Bound result 1124\n  Bound result 1\n  Bound result 43950335\n  Bound result 1577596\n  Bound result 84100245\n  Bound result 100000000\n  Bound result 88739\n  Bound result 43950335\n  Bound result 6\n  Bound result 3089\n  Bound result 4056\n  Bound result 53242879119560358\n  Bound result 8824\n  Bound result 19680\n  Bound result 100000000\n  Bound result 9680\n  Bound result 100000000\n  Bound result 11838\n  Bound result 6888\n  Bound result 33571852\n  Bound result 1115\n  Bound result 1500000000000010008\n  Bound result 1000000000000001\n  Bound result 514264337593543950335\n  Bound result 2514264337593529545215\n  Bound result 1294443463089300667\n  Bound result 28542344775283\n  Bound result 43950335\n  Bound result 514264337593543950335\n  Bound result 3855315688369783692\n  Bound result 1000000000\n  Bound result 575653\n  Bound result 543950335\n  Bound result 100000000\n  Bound result 1\n  Bound result 1\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 1\n  Bound result 54202146\n\n[PASS] test_handlerReachesLiquidationWriteoffRecoveryAndExit() (gas: 3624379)\nLogs:\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 86400\n  Bound result 1000000000000000000\n  Bound result 61098287\n  Bound result 1\n  Bound result 10000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 9.33s (9.33s CPU time)\n\nRan 14 test suites in 9.33s (13.25s CPU time): 90 tests passed, 0 failed, 1 skipped (91 total tests)\n","passed":true},{"durationMs":61,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GovernanceTimelock.cancel(bytes32)\",\"GovernanceTimelock.execute(address,bytes,bytes32)\",\"GovernanceTimelock.schedule(address,bytes,bytes32)\",\"IMDBank.accrue(address)\",\"IMDBank.borrow(address,uint256,address)\",\"IMDBank.configureReserve(address,uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.configureRisk(uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.coverBadDebt(address,uint256)\",\"IMDBank.donateLiquidity(address,uint256)\",\"IMDBank.finalizeDust(address)\",\"IMDBank.liquidate(address,address,uint256,uint256,uint256)\",\"IMDBank.repay(address,uint256,address)\",\"IMDBank.setCollateralEnabled(bool)\",\"IMDBank.setFrozen(bool)\",\"IMDBank.setGuardian(address)\",\"IMDBank.setReserveFrozen(address,bool)\",\"IMDBank.supply(uint256,address)\",\"IMDBank.withdraw(uint256,address)\",\"RiskOracle.configure(address,address,address,uint32,uint32,uint16,uint256,uint256,bool)\",\"RiskOracle.setEnabled(address,bool)\"],\"files\":{\".gitignore\":6,\"README.md\":53,\"docs/ARCHITECTURE.md\":77,\"docs/DEPENDENCIES.md\":8,\"docs/DEPLOYMENT.md\":72,\"docs/FRONTEND.md\":80,\"docs/RESEARCH.md\":105,\"docs/SECURITY_REVIEW.md\":85,\"docs/VALIDATION.md\":64,\"docs/evidence/aave-legacy-license.json\":8,\"docs/evidence/aave-legacy-tag.json\":14,\"docs/evidence/aave-origin-commit.json\":21,\"docs/evidence/aave-origin-license.json\":8,\"docs/evidence/bytecode-check.txt\":3,\"docs/evidence/chainlink-mainnet-catalog.json\":150,\"docs/evidence/chainlink-onchain.json\":91,\"docs/evidence/economic-stress.csv\":81,\"docs/evidence/forge-build.txt\":396,\"docs/evidence/forge-fmt.txt\":0,\"docs/evidence/forge-tests.txt\":95,\"docs/evidence/fork-rpc-limitation.json\":8,\"docs/evidence/frontend-abi-check.txt\":1,\"docs/evidence/frontend-fork-initial.json\":46,\"docs/evidence/frontend-fork.json\":43,\"docs/evidence/frontend-unit-tests.txt\":20,\"docs/evidence/imd-admin-holders-onchain.json\":108,\"docs/evidence/imd-blockrazor.json\":53,\"docs/evidence/imd-dexscreener.json\":7,\"docs/evidence/imd-etherscan.json\":5,\"docs/evidence/imd-holders-blockscout.json\":264,\"docs/evidence/imd-onchain.json\":41,\"docs/evidence/imd-sourcify.json\":269,\"docs/evidence/imd-token-blockscout.json\":19,\"docs/evidence/independent-review-checks.json\":41,\"docs/evidence/mainnet-block.json\":20,\"docs/evidence/mainnet-fork-tests.txt\":11,\"docs/evidence/usdc-blockrazor.json\":49,\"docs/evidence/usdc-onchain.json\":71,\"docs/evidence/usdt-onchain.json\":59,\"docs/evidence/weth-onchain.json\":59,\"foundry.toml\":26,\"remappings.txt\":2,\"script/DeployMainnet.s.sol\":41,\"src/GovernanceTimelock.sol\":76,\"src/IMDBank.sol\":617,\"src/RiskOracle.sol\":126,\"src/lib/ExactToken.sol\":58,\"test-fork/Mainnet.t.sol\":144,\"test/ADDED_TEST_COVERAGE.md\":81,\"test/AccountingEdges.t.sol\":350,\"test/Audit.t.sol\":189,\"test/DeploymentRehearsal.t.sol\":56,\"test/GovernanceEdges.t.sol\":238,\"test/GovernanceTimelock.t.sol\":77,\"test/IMDBank.t.sol\":255,\"test/Invariants.t.sol\":145,\"test/OptionalMainnet.t.sol\":137,\"test/OracleIntegrationEdges.t.sol\":287,\"test/RiskControls.t.sol\":101,\"test/RiskOracle.t.sol\":116,\"test/ThreeReserveInvariants.t.sol\":379,\"test/TokenBoundaryEdges.t.sol\":267,\"test/helpers/BankFixture.sol\":78,\"test/helpers/Mocks.sol\":126,\"tools/check_bytecode.py\":22,\"tools/economic_stress.py\":49,\"web/_headers\":11,\"web/abi.js\":33,\"web/app.js\":366,\"web/check-abi.mjs\":2,\"web/config.json\":19,\"web/core.js\":85,\"web/fonts.css\":1,\"web/index.html\":64,\"web/package.json\":9,\"web/styles.css\":2,\"web/tests/abi-compatibility.mjs\":20,\"web/tests/core.test.mjs\":90,\"web/tests/fork-integration.mjs\":162,\"web/vendor/ETHERS-LICENSE.md\":21,\"web/vendor/README.md\":10,\"web/vendor/ethers-6.15.0.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"49a36f82c701b679436131675cfb5e0922bb5793a63237dd8c2918033ef9ec7d","verifiedTreeHash":"71a0723d5d795fa4670401cdf39e6d81dac9142a","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":2402,"exitCode":0,"name":"build","output":"Compiling 36 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.23s\nCompiler run successful!\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:88:9\n   │\n88 │         emit FeedConfigured(asset, primary, secondary, collateralSide);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:89:9\n   │\n89 │         emit FeedEnabled(asset, true);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:97:9\n   │\n97 │         emit FeedEnabled(asset, enabled);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:119:78\n    │\n119 │             answer <= 0 || round == 0 || answered < round || updated == 0 || updated > block.timestamp\n    │                                                                              ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:120:57\n    │\n120 │                 || started == 0 || started > updated || block.timestamp - updated > maxAge\n    │                                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:123:13\n    │\n123 │         if (uint256(answer) > 1e36 / 10 ** (18 - decimals_)) revert InvalidPrice();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:124:16\n    │\n124 │         return uint256(answer) * 10 ** (18 - decimals_);\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `executing` is updated\n   ╭▸ src/GovernanceTimelock.sol:70:44\n   │\n70 │         (bool ok, bytes memory returned) = target.call(data);\n   │                                            ━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:62:51\n   │\n62 │             executing || done[id] || when == 0 || block.timestamp < when\n   │                                                   ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:63:20\n   │\n63 │                 || block.timestamp > when + GRACE_PERIOD\n   │                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/GovernanceTimelock.sol:55:22\n   │\n55 │     function execute(address target, bytes calldata data, bytes32 salt)\n   │                      ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/GovernanceTimelock.sol:73:9\n   │\n73 │         emit Executed(id);\n   │         ━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:22:26\n   │\n22 │         uint8 decimals = IExactERC20(token).decimals();\n   │                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:134:9\n    │\n134 │         address collateral_,\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:135:9\n    │\n135 │         address oracle_,\n    │         ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:154:64\n    │\n154 │             if (asset == collateral_ || assetUnit[asset] != 0) revert InvalidConfiguration();\n    │                                                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:21:37\n   │\n21 │         if (token.code.length == 0) revert InvalidToken();\n   │                                     ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:23:28\n   │\n23 │         if (decimals > 18) revert InvalidToken();\n   │                            ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:200:9\n    │\n200 │         emit Withdrawn(msg.sender, to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:218:9\n    │\n218 │         emit Borrowed(msg.sender, asset, to, amount, shares);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:237:9\n    │\n237 │         emit Repaid(msg.sender, onBehalfOf, asset, paid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:246:9\n    │\n246 │         emit LiquidityDonated(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:257:9\n    │\n257 │         emit BadDebtCovered(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:272:13\n    │\n272 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:512:13\n    │\n512 │             emit BadDebtRecorded(account, asset, amount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:513:13\n    │\n513 │             emit FrozenStateChanged(asset, true);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:514:13\n    │\n514 │             emit FrozenStateChanged(address(0), true);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:286:9\n    │\n286 │         emit Liquidated(msg.sender, account, asset, repaid, seized);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:308:9\n    │\n308 │         emit DustFinalized(msg.sender, account, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMDBank.sol:597:18\n    │\n597 │         price_ = IBankOracle(oracle).price(token);\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:602:36\n    │\n602 │         if (assetUnit[asset] == 0) revert UnsupportedAsset();\n    │                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:562:13\n    │\n562 │             revert InvalidConfiguration();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:598:48\n    │\n598 │         if (price_ == 0 || price_ > MAX_PRICE) revert InvalidPrice();\n    │                                                ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:416:9\n    │\n416 │         emit ReserveConfigured(asset, borrowCap, baseRateRay, slope1Ray, slope2Ray, kinkBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:423:55\n    │\n423 │                 if (reserves[assets[i]].badDebt != 0) revert OutstandingBadDebt();\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:495:28\n    │\n495 │             if (paid != 0) revert InvalidAmount();\n    │                            ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:17:16\n   │\n17 │         return IExactERC20(token).balanceOf(account);\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:555:9\n    │\n555 │         emit Accrued(asset, reserve.index, reserve.cachedRateRay);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:559:45\n    │\n559 │         if (reserve.totalDebtShares == 0 || block.timestamp == reserve.lastAccrual) return reserve.index;\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:561:13\n    │\n561 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:561:51\n    │\n561 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:569:16\n    │\n569 │         while (elapsed != 0) {\n    │                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:570:17\n    │\n570 │             if (elapsed & 1 != 0) accumulated = _rayMulCapped(accumulated, factor);\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:572:17\n    │\n572 │             if (elapsed != 0) factor = _rayMulCapped(factor, factor);\n    │                 ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:51:17\n   │\n51 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:61:17\n   │\n61 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/RiskControls.t.sol:61:21\n   │\n61 │         uint256 t = block.timestamp;\n   │                     ━━━━━━━━━━━━━━━\n62 │         vm.warp(t + 30 days);\n   │         ──────────────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":1486,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/DeploymentRehearsal.t.sol:DeploymentRehearsalTest\n[PASS] test_explicitRolesFromFactoryLikeCallerAndTimelockedRisk() (gas: 11710046)\n[PASS] test_wrongChainAndEOARolesRejected() (gas: 5110289)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.71ms (1.77ms CPU time)\n\nRan 4 tests for test/GovernanceTimelock.t.sol:GovernanceTimelockTest\n[PASS] test_cancellationAndAccess() (gas: 210103)\n[PASS] test_delayPermissionlessExecutionAndReplay() (gas: 266474)\n[PASS] test_domainSeparation() (gas: 22790)\n[PASS] test_expiryAndFailureAtomicity() (gas: 200959)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 2.67ms (887.73µs CPU time)\n\nRan 7 tests for test/RiskControls.t.sol:RiskControlsTest\n[PASS] test_capReductionCannotBlockRepayment() (gas: 676247)\n[PASS] test_hardRiskBoundsCannotBeBypassedByGovernor() (gas: 154047)\n[PASS] test_liquidityExhaustionRevertsWithoutMintingDebt() (gas: 318099)\n[PASS] test_lossRecognitionRecapitalizationAndRestart() (gas: 990244)\n[PASS] test_multiReserveDebtWrittenOffAfterCollateralExhausted() (gas: 1299263)\n[PASS] test_permissionlessLiquidationFitsGasBudget() (gas: 605718)\n[PASS] test_tokenBalanceDonationCannotRetroactivelyChangeRate() (gas: 491413)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 24.53ms (5.74ms CPU time)\n\nRan 7 tests for test/RiskOracle.t.sol:RiskOracleTest\n[PASS] testFuzz_agreementAlwaysUsesSafeSide(uint64,uint64) (runs: 512, μ: 334533, ~: 334401)\nLogs:\n  Bound result 677646271\n  Bound result 37895873\n\n[PASS] test_ageBoundaryAndCircuitBreaker() (gas: 271330)\n[PASS] test_conservativeDirectionAndDecimals() (gas: 325304)\n[PASS] test_depegIsPricedNotAssumedOneDollar() (gas: 194230)\n[PASS] test_feedDecimalsCannotSilentlyChange() (gas: 66234)\n[PASS] test_governanceAndGuardianSeparation() (gas: 251828)\n[PASS] test_rejectsBadRoundsAndTimes() (gas: 470682)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 28.84ms (30.13ms CPU time)\n\nRan 9 tests for test/Audit.t.sol:IndependentAuditTest\n[PASS] testAudit_actualTinyLossTriggersGlobalSafetyHalt() (gas: 771740)\n[PASS] testAudit_checkpointFrequencyCannotMateriallyChangeBorrowerDebt() (gas: 22765073)\n[PASS] testAudit_collateralCallbackCannotLiquidateOrBorrow() (gas: 328890)\n[PASS] testAudit_dustFinalizationRejectsHealthyValuableAndInvalidPrice() (gas: 421454)\n[PASS] testAudit_dustTransferFailureRollsBackWriteoff() (gas: 397286)\n[PASS] testAudit_extremeCollapseRecordsBadDebt() (gas: 332121)\n[PASS] testAudit_insolventButPartlyRecoverableDustMustLiquidateFirst() (gas: 254035)\n[PASS] testAudit_oracleConfigurationRollsBackIfBroken() (gas: 1029291)\n[PASS] testAudit_smallRecoverablePositionCannotTriggerGlobalFreeze() (gas: 970961)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 34.73ms (27.81ms CPU time)\n\nRan 17 tests for test/IMDBank.t.sol:IMDBankTest\n[PASS] testFuzz_borrowRepayRoundingCannotCreateAssets(uint64,uint32) (runs: 512, μ: 564880, ~: 564071)\nLogs:\n  Bound result 4696\n  Bound result 13191\n\n[PASS] testFuzz_roundTripNoCollateralGain(uint96) (runs: 512, μ: 222885, ~: 222695)\nLogs:\n  Bound result 2\n\n[PASS] test_completeLifecycleThreeReserves() (gas: 1429741)\n[PASS] test_flashSupplyBorrowWithdrawCannotEscapeDebt() (gas: 482815)\n[PASS] test_guardianCannotUnfreezeOrChangeRisk() (gas: 758192)\n[PASS] test_initialDeploymentCannotTakeRisk() (gas: 125602)\n[PASS] test_liquidationSlippageAndExpiryProtectPayer() (gas: 574550)\n[PASS] test_noReturnTokensWorkAndFalseReturnRejected() (gas: 620428)\n[PASS] test_oracleOutageStillAllowsRepaymentAndDebtFreeExit() (gas: 764952)\n[PASS] test_partialRepayNeverReducesDebtMoreThanPayment() (gas: 567950)\n[PASS] test_priceDropLiquidationAndCloseFactor() (gas: 867321)\n[PASS] test_reentrancyTransferCallbackCannotMintUnbackedReceipts() (gas: 313832)\n[PASS] test_rejectsOverborrowWithdrawalAndCollateralDisable() (gas: 740270)\n[PASS] test_safeHealthRejectsLiquidation() (gas: 488508)\n[PASS] test_stablecoinDepegAndWethSpikeChangeDebtValue() (gas: 780072)\n[PASS] test_supplyOnBehalfDoesNotEnableOthersCollateral() (gas: 173849)\n[PASS] test_taxedTransfersFailAtomicallyInAndOut() (gas: 772536)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 40.16ms (68.18ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BankInvariantTest\n[PASS]\nBankInvariantTest invariants:\n[PASS] invariant_cashFlowConservation\n[PASS] invariant_collateralConservation\n[PASS] invariant_debtSharesAndAggregateRounding\n BankInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------+-------------------+-------+---------+----------╮\n| Contract    | Selector          | Calls | Reverts | Discards |\n+==============================================================+\n| BankHandler | borrow            | 1121  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | donate            | 1162  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | elapse            | 1146  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | repay             | 1113  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | shockAndLiquidate | 1189  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | supply            | 1224  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | withdraw          | 1237  | 0       | 0        |\n╰-------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 50\n  Bound result 3664\n  Bound result 10000\n  Bound result 3463\n  Bound result 455\n  Bound result 1447\n  Bound result 13\n  Bound result 5\n  Bound result 11164\n  Bound result 11117\n  Bound result 2766\n  Bound result 10905\n  Bound result 3987\n  Bound result 16\n  Bound result 80000000\n  Bound result 86400\n  Bound result 3\n  Bound result 4185\n  Bound result 99\n  Bound result 6847\n  Bound result 5412699765\n  Bound result 10000\n  Bound result 5000\n  Bound result 3762\n  Bound result 693\n  Bound result 168\n  Bound result 1254681438\n  Bound result 1147835013378853\n  Bound result 5203\n  Bound result 421201\n  Bound result 1558899139\n  Bound result 2554371944117425887\n  Bound result 9895\n  Bound result 823220532\n  Bound result 1543\n  Bound result 647969\n  Bound result 10286337614\n  Bound result 7882\n  Bound result 8000\n  Bound result 2000000000\n  Bound result 4836\n  Bound result 2\n  Bound result 8000\n  Bound result 5315\n  Bound result 1000\n  Bound result 6364\n  Bound result 1510598867\n  Bound result 2272\n  Bound result 88382554413\n  Bound result 10081\n  Bound result 8262\n  Bound result 4107\n  Bound result 835717307\n  Bound result 10000000000\n  Bound result 11108\n  Bound result 1217\n  Bound result 21638\n  Bound result 2323\n  Bound result 2197\n  Bound result 31536000\n  Bound result 50\n  Bound result 3600\n  Bound result 13\n  Bound result 31536000\n  Bound result 1\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.40s (1.39s CPU time)\n\nRan 7 test suites in 1.40s (1.53s CPU time): 47 tests passed, 0 failed, 0 skipped (47 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GovernanceTimelock.cancel(bytes32)\",\"GovernanceTimelock.execute(address,bytes,bytes32)\",\"GovernanceTimelock.schedule(address,bytes,bytes32)\",\"IMDBank.accrue(address)\",\"IMDBank.borrow(address,uint256,address)\",\"IMDBank.configureReserve(address,uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.configureRisk(uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.coverBadDebt(address,uint256)\",\"IMDBank.donateLiquidity(address,uint256)\",\"IMDBank.finalizeDust(address)\",\"IMDBank.liquidate(address,address,uint256,uint256,uint256)\",\"IMDBank.repay(address,uint256,address)\",\"IMDBank.setCollateralEnabled(bool)\",\"IMDBank.setFrozen(bool)\",\"IMDBank.setGuardian(address)\",\"IMDBank.setReserveFrozen(address,bool)\",\"IMDBank.supply(uint256,address)\",\"IMDBank.withdraw(uint256,address)\",\"RiskOracle.configure(address,address,address,uint32,uint32,uint16,uint256,uint256,bool)\",\"RiskOracle.setEnabled(address,bool)\"],\"files\":{\".gitignore\":6,\"README.md\":53,\"docs/ARCHITECTURE.md\":77,\"docs/DEPENDENCIES.md\":8,\"docs/DEPLOYMENT.md\":72,\"docs/FRONTEND.md\":80,\"docs/RESEARCH.md\":105,\"docs/SECURITY_REVIEW.md\":85,\"docs/VALIDATION.md\":64,\"docs/evidence/aave-legacy-license.json\":8,\"docs/evidence/aave-legacy-tag.json\":14,\"docs/evidence/aave-origin-commit.json\":21,\"docs/evidence/aave-origin-license.json\":8,\"docs/evidence/bytecode-check.txt\":3,\"docs/evidence/chainlink-mainnet-catalog.json\":150,\"docs/evidence/chainlink-onchain.json\":91,\"docs/evidence/economic-stress.csv\":81,\"docs/evidence/forge-build.txt\":396,\"docs/evidence/forge-fmt.txt\":0,\"docs/evidence/forge-tests.txt\":95,\"docs/evidence/fork-rpc-limitation.json\":8,\"docs/evidence/frontend-abi-check.txt\":1,\"docs/evidence/frontend-fork-initial.json\":46,\"docs/evidence/frontend-fork.json\":43,\"docs/evidence/frontend-unit-tests.txt\":20,\"docs/evidence/imd-admin-holders-onchain.json\":108,\"docs/evidence/imd-blockrazor.json\":53,\"docs/evidence/imd-dexscreener.json\":7,\"docs/evidence/imd-etherscan.json\":5,\"docs/evidence/imd-holders-blockscout.json\":264,\"docs/evidence/imd-onchain.json\":41,\"docs/evidence/imd-sourcify.json\":269,\"docs/evidence/imd-token-blockscout.json\":19,\"docs/evidence/independent-review-checks.json\":41,\"docs/evidence/mainnet-block.json\":20,\"docs/evidence/mainnet-fork-tests.txt\":11,\"docs/evidence/usdc-blockrazor.json\":49,\"docs/evidence/usdc-onchain.json\":71,\"docs/evidence/usdt-onchain.json\":59,\"docs/evidence/weth-onchain.json\":59,\"foundry.toml\":26,\"launch.json\":33,\"remappings.txt\":2,\"script/DeployMainnet.s.sol\":41,\"src/GovernanceTimelock.sol\":76,\"src/IMDBank.sol\":617,\"src/RiskOracle.sol\":126,\"src/lib/ExactToken.sol\":58,\"test-fork/Mainnet.t.sol\":144,\"test/Audit.t.sol\":189,\"test/DeploymentRehearsal.t.sol\":56,\"test/GovernanceTimelock.t.sol\":77,\"test/IMDBank.t.sol\":255,\"test/Invariants.t.sol\":145,\"test/RiskControls.t.sol\":101,\"test/RiskOracle.t.sol\":116,\"test/helpers/BankFixture.sol\":78,\"test/helpers/Mocks.sol\":126,\"tools/check_bytecode.py\":22,\"tools/economic_stress.py\":49,\"web/_headers\":11,\"web/abi.js\":33,\"web/app.js\":366,\"web/check-abi.mjs\":2,\"web/config.json\":19,\"web/core.js\":85,\"web/fonts.css\":1,\"web/index.html\":64,\"web/package.json\":9,\"web/styles.css\":2,\"web/tests/abi-compatibility.mjs\":20,\"web/tests/core.test.mjs\":90,\"web/tests/fork-integration.mjs\":162,\"web/vendor/ETHERS-LICENSE.md\":21,\"web/vendor/README.md\":10,\"web/vendor/ethers-6.15.0.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"5ef5b3838f0f7f9d184d038dce724fabe20e06d28b237484ead0bf5879f3ed30","verifiedTreeHash":"c14e412d8eb9dc7c924f12f033c2ffd3c6876377","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":2981,"exitCode":0,"name":"build","output":"Compiling 36 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.79s\nCompiler run successful!\nwarning[reentrancy-no-eth]: external call can be reentered before `executing` is updated\n   ╭▸ src/GovernanceTimelock.sol:70:44\n   │\n70 │         (bool ok, bytes memory returned) = target.call(data);\n   │                                            ━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:62:51\n   │\n62 │             executing || done[id] || when == 0 || block.timestamp < when\n   │                                                   ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:63:20\n   │\n63 │                 || block.timestamp > when + GRACE_PERIOD\n   │                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:104:9\n    │\n104 │         emit FeedConfigured(asset, primary, secondary, collateralSide);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:105:9\n    │\n105 │         emit FeedEnabled(asset, true);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/GovernanceTimelock.sol:55:22\n   │\n55 │     function execute(address target, bytes calldata data, bytes32 salt)\n   │                      ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/GovernanceTimelock.sol:73:9\n   │\n73 │         emit Executed(id);\n   │         ━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:117:13\n    │\n117 │             emit FeedEnabled(asset, enabled);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:121:37\n    │\n121 │             f.guardianPausedUntil = uint64(until);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint64' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:144:27\n    │\n144 │         if (!f.enabled || block.timestamp < f.guardianPausedUntil) revert Disabled();\n    │                           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:161:78\n    │\n161 │             answer <= 0 || round == 0 || answered < round || updated == 0 || updated > block.timestamp\n    │                                                                              ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:162:57\n    │\n162 │                 || started == 0 || started > updated || block.timestamp - updated > maxAge\n    │                                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:165:13\n    │\n165 │         if (uint256(answer) > 1e36 / 10 ** (18 - decimals_)) revert InvalidPrice();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:166:16\n    │\n166 │         return uint256(answer) * 10 ** (18 - decimals_);\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:22:26\n   │\n22 │         uint8 decimals = IExactERC20(token).decimals();\n   │                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:142:9\n    │\n142 │         address collateral_,\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:143:9\n    │\n143 │         address oracle_,\n    │         ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:162:64\n    │\n162 │             if (asset == collateral_ || assetUnit[asset] != 0) revert InvalidConfiguration();\n    │                                                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:21:37\n   │\n21 │         if (token.code.length == 0) revert InvalidToken();\n   │                                     ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:23:28\n   │\n23 │         if (decimals > 18) revert InvalidToken();\n   │                            ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:209:9\n    │\n209 │         emit Withdrawn(msg.sender, to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:230:9\n    │\n230 │         emit Borrowed(msg.sender, asset, to, amount, shares);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:249:9\n    │\n249 │         emit Repaid(msg.sender, onBehalfOf, asset, paid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:258:9\n    │\n258 │         emit LiquidityDonated(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:270:9\n    │\n270 │         emit BadDebtCovered(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:285:13\n    │\n285 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:529:13\n    │\n529 │             emit BadDebtRecorded(account, asset, amount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:536:17\n    │\n536 │                 emit FrozenStateChanged(asset, true);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:537:17\n    │\n537 │                 emit FrozenStateChanged(address(0), true);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:299:9\n    │\n299 │         emit Liquidated(msg.sender, account, asset, repaid, seized);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:321:9\n    │\n321 │         emit DustFinalized(msg.sender, account, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMDBank.sol:625:18\n    │\n625 │         price_ = IBankOracle(oracle).price(token);\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:630:36\n    │\n630 │         if (assetUnit[asset] == 0) revert UnsupportedAsset();\n    │                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:590:13\n    │\n590 │             revert InvalidConfiguration();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:626:48\n    │\n626 │         if (price_ == 0 || price_ > MAX_PRICE) revert InvalidPrice();\n    │                                                ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:434:9\n    │\n434 │         emit ReserveConfigured(asset, borrowCap, baseRateRay, slope1Ray, slope2Ray, kinkBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:441:51\n    │\n441 │                 if (reserves[assets[i]].lossHalt) revert OutstandingBadDebt();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:514:28\n    │\n514 │             if (paid != 0) revert InvalidAmount();\n    │                            ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:17:16\n   │\n17 │         return IExactERC20(token).balanceOf(account);\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:583:9\n    │\n583 │         emit Accrued(asset, reserve.index, reserve.cachedRateRay);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:587:45\n    │\n587 │         if (reserve.totalDebtShares == 0 || block.timestamp == reserve.lastAccrual) return reserve.index;\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:589:13\n    │\n589 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:589:51\n    │\n589 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:597:16\n    │\n597 │         while (elapsed != 0) {\n    │                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:598:17\n    │\n598 │             if (elapsed & 1 != 0) accumulated = _rayMulCapped(accumulated, factor);\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:600:17\n    │\n600 │             if (elapsed != 0) factor = _rayMulCapped(factor, factor);\n    │                 ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:51:17\n   │\n51 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:61:17\n   │\n61 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/RiskControls.t.sol:70:21\n   │\n70 │         uint256 t = block.timestamp;\n   │                     ━━━━━━━━━━━━━━━\n71 │         vm.warp(t + 30 days);\n   │         ──────────────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":1675,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/DeploymentRehearsal.t.sol:DeploymentRehearsalTest\n[PASS] test_explicitRolesFromFactoryLikeCallerAndTimelockedRisk() (gas: 12475338)\n[PASS] test_wrongChainEOAAndSharedRolesRejected() (gas: 5798019)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.03ms (1.25ms CPU time)\n\nRan 4 tests for test/GovernanceTimelock.t.sol:GovernanceTimelockTest\n[PASS] test_cancellationAndAccess() (gas: 210103)\n[PASS] test_delayPermissionlessExecutionAndReplay() (gas: 266474)\n[PASS] test_domainSeparation() (gas: 22790)\n[PASS] test_expiryAndFailureAtomicity() (gas: 200959)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.20ms (1.03ms CPU time)\n\nRan 11 tests for test/RiskOracle.t.sol:RiskOracleTest\n[PASS] testFuzz_agreementAlwaysUsesSafeSide(uint64,uint64) (runs: 512, μ: 335551, ~: 335333)\nLogs:\n  Bound result 890077126\n  Bound result 10\n\n[PASS] test_ageBoundAllowsHeartbeatPlusGrace() (gas: 347389)\n[PASS] test_ageBoundaryAndCircuitBreaker() (gas: 271627)\n[PASS] test_boundsRejectOnlyBorrowerOvervaluation() (gas: 637518)\n[PASS] test_conservativeDirectionAndDecimals() (gas: 326271)\n[PASS] test_depegIsPricedNotAssumedOneDollar() (gas: 194740)\n[PASS] test_feedDecimalsCannotSilentlyChange() (gas: 66330)\n[PASS] test_governanceAndGuardianSeparation() (gas: 265582)\n[PASS] test_guardianPauseExpiresOnceAndGovernanceRearms() (gas: 603417)\n[PASS] test_guardianRotationAndPauseBounds() (gas: 303376)\n[PASS] test_rejectsBadRoundsAndTimes() (gas: 471463)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 23.34ms (27.24ms CPU time)\n\nRan 10 tests for test/Audit.t.sol:IndependentAuditTest\n[PASS] testAudit_checkpointFrequencyCannotMateriallyChangeBorrowerDebt() (gas: 22740804)\n[PASS] testAudit_collateralCallbackCannotLiquidateOrBorrow() (gas: 328999)\n[PASS] testAudit_dustFinalizationRejectsHealthyValuableAndInvalidPrice() (gas: 421477)\n[PASS] testAudit_dustTransferFailureRollsBackWriteoff() (gas: 399889)\n[PASS] testAudit_extremeCollapseRecordsBadDebt() (gas: 334702)\n[PASS] testAudit_insolventButPartlyRecoverableDustMustLiquidateFirst() (gas: 253994)\n[PASS] testAudit_minimumDebtAndDustLossDoNotHaltLending() (gas: 1887540)\n[PASS] testAudit_oracleConfigurationRollsBackIfBroken() (gas: 1030185)\n[PASS] testAudit_seizureFlooringToZeroStillSweepsResidue() (gas: 1625768)\n[PASS] testAudit_smallRecoverablePositionCannotTriggerGlobalFreeze() (gas: 971931)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 40.30ms (23.58ms CPU time)\n\nRan 7 tests for test/RiskControls.t.sol:RiskControlsTest\n[PASS] test_capReductionCannotBlockRepayment() (gas: 675227)\n[PASS] test_hardRiskBoundsCannotBeBypassedByGovernor() (gas: 154202)\n[PASS] test_liquidityExhaustionRevertsWithoutMintingDebt() (gas: 315876)\n[PASS] test_lossRecognitionRecapitalizationAndRestart() (gas: 1174412)\n[PASS] test_multiReserveDebtWrittenOffAfterCollateralExhausted() (gas: 1309899)\n[PASS] test_permissionlessLiquidationFitsGasBudget() (gas: 606658)\n[PASS] test_tokenBalanceDonationCannotRetroactivelyChangeRate() (gas: 492373)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 40.30ms (4.86ms CPU time)\n\nRan 17 tests for test/IMDBank.t.sol:IMDBankTest\n[PASS] testFuzz_borrowRepayRoundingCannotCreateAssets(uint64,uint32) (runs: 512, μ: 568152, ~: 566786)\nLogs:\n  Bound result 2499013709\n  Bound result 1822\n\n[PASS] testFuzz_roundTripNoCollateralGain(uint96) (runs: 512, μ: 222854, ~: 222738)\nLogs:\n  Bound result 2805970705857624\n\n[PASS] test_completeLifecycleThreeReserves() (gas: 1433109)\n[PASS] test_flashSupplyBorrowWithdrawCannotEscapeDebt() (gas: 483863)\n[PASS] test_guardianCannotUnfreezeOrChangeRisk() (gas: 759157)\n[PASS] test_initialDeploymentCannotTakeRisk() (gas: 125584)\n[PASS] test_liquidationSlippageAndExpiryProtectPayer() (gas: 575358)\n[PASS] test_noReturnTokensWorkAndFalseReturnRejected() (gas: 621499)\n[PASS] test_oracleOutageStillAllowsRepaymentAndDebtFreeExit() (gas: 764020)\n[PASS] test_partialRepayNeverReducesDebtMoreThanPayment() (gas: 568911)\n[PASS] test_priceDropLiquidationAndCloseFactor() (gas: 868219)\n[PASS] test_reentrancyTransferCallbackCannotMintUnbackedReceipts() (gas: 313788)\n[PASS] test_rejectsOverborrowWithdrawalAndCollateralDisable() (gas: 739314)\n[PASS] test_safeHealthRejectsLiquidation() (gas: 489468)\n[PASS] test_stablecoinDepegAndWethSpikeChangeDebtValue() (gas: 782234)\n[PASS] test_supplyOnBehalfDoesNotEnableOthersCollateral() (gas: 173849)\n[PASS] test_taxedTransfersFailAtomicallyInAndOut() (gas: 773562)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 40.32ms (66.19ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BankInvariantTest\n[PASS]\nBankInvariantTest invariants:\n[PASS] invariant_cashFlowConservation\n[PASS] invariant_collateralConservation\n[PASS] invariant_debtSharesAndAggregateRounding\n BankInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------+-------------------+-------+---------+----------╮\n| Contract    | Selector          | Calls | Reverts | Discards |\n+==============================================================+\n| BankHandler | borrow            | 1156  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | donate            | 1177  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | elapse            | 1227  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | repay             | 1158  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | shockAndLiquidate | 1151  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | supply            | 1136  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | withdraw          | 1187  | 0       | 0        |\n╰-------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1313373041\n  Bound result 2915\n  Bound result 8118\n  Bound result 2009\n  Bound result 2009\n  Bound result 8458\n  Bound result 629\n  Bound result 11279\n  Bound result 4678197\n  Bound result 1168\n  Bound result 247800479072196\n  Bound result 5781\n  Bound result 17436983\n  Bound result 1000\n  Bound result 431\n  Bound result 1\n  Bound result 4171\n  Bound result 2262736462\n  Bound result 7450\n  Bound result 7161\n  Bound result 389724784\n  Bound result 2400\n  Bound result 900000000\n  Bound result 17951\n  Bound result 7490909495\n  Bound result 2216\n  Bound result 513\n  Bound result 3548\n  Bound result 7478\n  Bound result 9911\n  Bound result 12048\n  Bound result 2925127636516\n  Bound result 35339\n  Bound result 7546\n  Bound result 789\n  Bound result 3500\n  Bound result 59681110219\n  Bound result 10000\n  Bound result 3146\n  Bound result 10892\n  Bound result 11595\n  Bound result 386\n  Bound result 431\n  Bound result 24301\n  Bound result 634\n  Bound result 178\n  Bound result 53335\n  Bound result 3596\n  Bound result 242\n  Bound result 6718829569\n  Bound result 1200000000000000000\n  Bound result 2592001\n  Bound result 3731\n  Bound result 2045632\n  Bound result 5650\n  Bound result 2825990543\n  Bound result 3\n  Bound result 2332\n  Bound result 42\n  Bound result 2772\n  Bound result 5374294621064586105845\n  Bound result 125000\n  Bound result 6222\n  Bound result 125000\n  Bound result 16202\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.56s (1.56s CPU time)\n\nRan 7 test suites in 1.56s (1.71s CPU time): 52 tests passed, 0 failed, 0 skipped (52 total tests)\n","passed":true},{"durationMs":84,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GovernanceTimelock.cancel(bytes32)\",\"GovernanceTimelock.execute(address,bytes,bytes32)\",\"GovernanceTimelock.schedule(address,bytes,bytes32)\",\"IMDBank.accrue(address)\",\"IMDBank.borrow(address,uint256,address)\",\"IMDBank.configureReserve(address,uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.configureRisk(uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.coverBadDebt(address,uint256)\",\"IMDBank.donateLiquidity(address,uint256)\",\"IMDBank.finalizeDust(address)\",\"IMDBank.liquidate(address,address,uint256,uint256,uint256)\",\"IMDBank.repay(address,uint256,address)\",\"IMDBank.setCollateralEnabled(bool)\",\"IMDBank.setFrozen(bool)\",\"IMDBank.setGuardian(address)\",\"IMDBank.setReserveFrozen(address,bool)\",\"IMDBank.supply(uint256,address)\",\"IMDBank.withdraw(uint256,address)\",\"RiskOracle.configure(address,address,address,uint32,uint32,uint16,uint256,uint256,bool)\",\"RiskOracle.setEnabled(address,bool)\",\"RiskOracle.setGuardian(address)\",\"RiskOracle.setGuardianPause(uint256)\"],\"files\":{\".gitignore\":6,\"README.md\":53,\"docs/ARCHITECTURE.md\":85,\"docs/DEPENDENCIES.md\":8,\"docs/DEPLOYMENT.md\":73,\"docs/FRONTEND.md\":82,\"docs/RESEARCH.md\":105,\"docs/SECURITY_REVIEW.md\":122,\"docs/VALIDATION.md\":65,\"docs/evidence/aave-legacy-license.json\":8,\"docs/evidence/aave-legacy-tag.json\":14,\"docs/evidence/aave-origin-commit.json\":21,\"docs/evidence/aave-origin-license.json\":8,\"docs/evidence/bytecode-check.txt\":3,\"docs/evidence/chainlink-mainnet-catalog.json\":150,\"docs/evidence/chainlink-onchain.json\":91,\"docs/evidence/economic-stress.csv\":81,\"docs/evidence/forge-build.txt\":469,\"docs/evidence/forge-fmt.txt\":0,\"docs/evidence/forge-tests.txt\":100,\"docs/evidence/fork-rpc-limitation.json\":8,\"docs/evidence/frontend-abi-check.txt\":1,\"docs/evidence/frontend-fork-initial.json\":46,\"docs/evidence/frontend-fork-round1.json\":43,\"docs/evidence/frontend-fork.json\":41,\"docs/evidence/frontend-unit-tests.txt\":88,\"docs/evidence/imd-admin-holders-onchain.json\":108,\"docs/evidence/imd-blockrazor.json\":53,\"docs/evidence/imd-dexscreener.json\":7,\"docs/evidence/imd-etherscan.json\":5,\"docs/evidence/imd-holders-blockscout.json\":264,\"docs/evidence/imd-onchain.json\":41,\"docs/evidence/imd-sourcify.json\":269,\"docs/evidence/imd-token-blockscout.json\":19,\"docs/evidence/independent-review-checks.json\":47,\"docs/evidence/mainnet-block.json\":20,\"docs/evidence/mainnet-fork-tests.txt\":11,\"docs/evidence/usdc-blockrazor.json\":49,\"docs/evidence/usdc-onchain.json\":71,\"docs/evidence/usdt-onchain.json\":59,\"docs/evidence/weth-onchain.json\":59,\"foundry.toml\":26,\"launch.json\":33,\"remappings.txt\":2,\"script/DeployMainnet.s.sol\":51,\"src/GovernanceTimelock.sol\":76,\"src/IMDBank.sol\":645,\"src/RiskOracle.sol\":168,\"src/lib/ExactToken.sol\":58,\"test-fork/Mainnet.t.sol\":144,\"test/Audit.t.sol\":245,\"test/DeploymentRehearsal.t.sol\":67,\"test/GovernanceTimelock.t.sol\":77,\"test/IMDBank.t.sol\":255,\"test/Invariants.t.sol\":151,\"test/RiskControls.t.sol\":110,\"test/RiskOracle.t.sol\":217,\"test/helpers/BankFixture.sol\":78,\"test/helpers/Mocks.sol\":126,\"tools/check_bytecode.py\":22,\"tools/economic_stress.py\":49,\"web/_headers\":11,\"web/abi.js\":34,\"web/app.js\":371,\"web/check-abi.mjs\":2,\"web/config.json\":19,\"web/core.js\":91,\"web/fonts.css\":1,\"web/index.html\":64,\"web/package.json\":9,\"web/styles.css\":2,\"web/tests/abi-compatibility.mjs\":20,\"web/tests/core.test.mjs\":109,\"web/tests/fork-integration.mjs\":162,\"web/vendor/ETHERS-LICENSE.md\":21,\"web/vendor/README.md\":10,\"web/vendor/ethers-6.15.0.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"7cd29902101eb209fb89c69a9dfc06705560795999f59b6fbd85d05c161f4e63","verifiedTreeHash":"b3165e9ec7788d4bbda347091b61e481149b6f38","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":5773,"exitCode":0,"name":"build","output":"Compiling 42 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.46s\nCompiler run successful!\nwarning[reentrancy-no-eth]: external call can be reentered before `executing` is updated\n   ╭▸ src/GovernanceTimelock.sol:70:44\n   │\n70 │         (bool ok, bytes memory returned) = target.call(data);\n   │                                            ━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:62:51\n   │\n62 │             executing || done[id] || when == 0 || block.timestamp < when\n   │                                                   ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:63:20\n   │\n63 │                 || block.timestamp > when + GRACE_PERIOD\n   │                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/GovernanceTimelock.sol:55:22\n   │\n55 │     function execute(address target, bytes calldata data, bytes32 salt)\n   │                      ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:104:9\n    │\n104 │         emit FeedConfigured(asset, primary, secondary, collateralSide);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/GovernanceTimelock.sol:73:9\n   │\n73 │         emit Executed(id);\n   │         ━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:105:9\n    │\n105 │         emit FeedEnabled(asset, true);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:117:13\n    │\n117 │             emit FeedEnabled(asset, enabled);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:121:37\n    │\n121 │             f.guardianPausedUntil = uint64(until);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint64' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:144:27\n    │\n144 │         if (!f.enabled || block.timestamp < f.guardianPausedUntil) revert Disabled();\n    │                           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:161:78\n    │\n161 │             answer <= 0 || round == 0 || answered < round || updated == 0 || updated > block.timestamp\n    │                                                                              ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:162:57\n    │\n162 │                 || started == 0 || started > updated || block.timestamp - updated > maxAge\n    │                                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:165:13\n    │\n165 │         if (uint256(answer) > 1e36 / 10 ** (18 - decimals_)) revert InvalidPrice();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:166:16\n    │\n166 │         return uint256(answer) * 10 ** (18 - decimals_);\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:22:26\n   │\n22 │         uint8 decimals = IExactERC20(token).decimals();\n   │                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:143:9\n    │\n143 │         address oracle_,\n    │         ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:142:9\n    │\n142 │         address collateral_,\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:162:64\n    │\n162 │             if (asset == collateral_ || assetUnit[asset] != 0) revert InvalidConfiguration();\n    │                                                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:21:37\n   │\n21 │         if (token.code.length == 0) revert InvalidToken();\n   │                                     ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:23:28\n   │\n23 │         if (decimals > 18) revert InvalidToken();\n   │                            ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:209:9\n    │\n209 │         emit Withdrawn(msg.sender, to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:230:9\n    │\n230 │         emit Borrowed(msg.sender, asset, to, amount, shares);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:249:9\n    │\n249 │         emit Repaid(msg.sender, onBehalfOf, asset, paid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:258:9\n    │\n258 │         emit LiquidityDonated(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:270:9\n    │\n270 │         emit BadDebtCovered(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:285:13\n    │\n285 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:529:13\n    │\n529 │             emit BadDebtRecorded(account, asset, amount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:536:17\n    │\n536 │                 emit FrozenStateChanged(asset, true);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:537:17\n    │\n537 │                 emit FrozenStateChanged(address(0), true);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:299:9\n    │\n299 │         emit Liquidated(msg.sender, account, asset, repaid, seized);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:321:9\n    │\n321 │         emit DustFinalized(msg.sender, account, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMDBank.sol:625:18\n    │\n625 │         price_ = IBankOracle(oracle).price(token);\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:630:36\n    │\n630 │         if (assetUnit[asset] == 0) revert UnsupportedAsset();\n    │                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:590:13\n    │\n590 │             revert InvalidConfiguration();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:626:48\n    │\n626 │         if (price_ == 0 || price_ > MAX_PRICE) revert InvalidPrice();\n    │                                                ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:434:9\n    │\n434 │         emit ReserveConfigured(asset, borrowCap, baseRateRay, slope1Ray, slope2Ray, kinkBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:441:51\n    │\n441 │                 if (reserves[assets[i]].lossHalt) revert OutstandingBadDebt();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:514:28\n    │\n514 │             if (paid != 0) revert InvalidAmount();\n    │                            ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:17:16\n   │\n17 │         return IExactERC20(token).balanceOf(account);\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:583:9\n    │\n583 │         emit Accrued(asset, reserve.index, reserve.cachedRateRay);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:587:45\n    │\n587 │         if (reserve.totalDebtShares == 0 || block.timestamp == reserve.lastAccrual) return reserve.index;\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:589:13\n    │\n589 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:589:51\n    │\n589 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:597:16\n    │\n597 │         while (elapsed != 0) {\n    │                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:598:17\n    │\n598 │             if (elapsed & 1 != 0) accumulated = _rayMulCapped(accumulated, factor);\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:600:17\n    │\n600 │             if (elapsed != 0) factor = _rayMulCapped(factor, factor);\n    │                 ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:51:17\n   │\n51 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:61:17\n   │\n61 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/RiskControls.t.sol:70:21\n   │\n70 │         uint256 t = block.timestamp;\n   │                     ━━━━━━━━━━━━━━━\n71 │         vm.warp(t + 30 days);\n   │         ──────────────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":12126,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/OptionalMainnet.t.sol:OptionalMainnetTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 1.31ms (0.00ns CPU time)\n\nRan 2 tests for test/DeploymentRehearsal.t.sol:DeploymentRehearsalTest\n[PASS] test_explicitRolesFromFactoryLikeCallerAndTimelockedRisk() (gas: 12475338)\n[PASS] test_wrongChainEOAAndSharedRolesRejected() (gas: 5798019)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.07ms (1.87ms CPU time)\n\nRan 4 tests for test/GovernanceTimelock.t.sol:GovernanceTimelockTest\n[PASS] test_cancellationAndAccess() (gas: 210103)\n[PASS] test_delayPermissionlessExecutionAndReplay() (gas: 266474)\n[PASS] test_domainSeparation() (gas: 22790)\n[PASS] test_expiryAndFailureAtomicity() (gas: 200959)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 3.59ms (2.76ms CPU time)\n\nRan 7 tests for test/RiskControls.t.sol:RiskControlsTest\n[PASS] test_capReductionCannotBlockRepayment() (gas: 675227)\n[PASS] test_hardRiskBoundsCannotBeBypassedByGovernor() (gas: 154202)\n[PASS] test_liquidityExhaustionRevertsWithoutMintingDebt() (gas: 315876)\n[PASS] test_lossRecognitionRecapitalizationAndRestart() (gas: 1174412)\n[PASS] test_multiReserveDebtWrittenOffAfterCollateralExhausted() (gas: 1309899)\n[PASS] test_permissionlessLiquidationFitsGasBudget() (gas: 606658)\n[PASS] test_tokenBalanceDonationCannotRetroactivelyChangeRate() (gas: 492373)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 4.10ms (2.90ms CPU time)\n\nRan 17 tests for test/IMDBank.t.sol:IMDBankTest\n[PASS] testFuzz_borrowRepayRoundingCannotCreateAssets(uint64,uint32) (runs: 512, μ: 568890, ~: 568669)\nLogs:\n  Bound result 2499004282\n  Bound result 2827\n\n[PASS] testFuzz_roundTripNoCollateralGain(uint96) (runs: 512, μ: 222931, ~: 222760)\nLogs:\n  Bound result 960821728464\n\n[PASS] test_completeLifecycleThreeReserves() (gas: 1433109)\n[PASS] test_flashSupplyBorrowWithdrawCannotEscapeDebt() (gas: 483863)\n[PASS] test_guardianCannotUnfreezeOrChangeRisk() (gas: 759157)\n[PASS] test_initialDeploymentCannotTakeRisk() (gas: 125584)\n[PASS] test_liquidationSlippageAndExpiryProtectPayer() (gas: 575358)\n[PASS] test_noReturnTokensWorkAndFalseReturnRejected() (gas: 621499)\n[PASS] test_oracleOutageStillAllowsRepaymentAndDebtFreeExit() (gas: 764020)\n[PASS] test_partialRepayNeverReducesDebtMoreThanPayment() (gas: 568911)\n[PASS] test_priceDropLiquidationAndCloseFactor() (gas: 868219)\n[PASS] test_reentrancyTransferCallbackCannotMintUnbackedReceipts() (gas: 313788)\n[PASS] test_rejectsOverborrowWithdrawalAndCollateralDisable() (gas: 739314)\n[PASS] test_safeHealthRejectsLiquidation() (gas: 489468)\n[PASS] test_stablecoinDepegAndWethSpikeChangeDebtValue() (gas: 782234)\n[PASS] test_supplyOnBehalfDoesNotEnableOthersCollateral() (gas: 173849)\n[PASS] test_taxedTransfersFailAtomicallyInAndOut() (gas: 773562)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 649.87ms (887.86ms CPU time)\n\nRan 8 tests for test/GovernanceEdges.t.sol:TimelockEdgesTest\n[PASS] test_cancelledProposalCannotUseOriginalMaturityAfterReschedule() (gas: 290640)\n[PASS] test_constructorRejectsRoleCollisionsAndDelayOutOfBounds() (gas: 3968)\n[PASS] test_domainBindsExecutorTargetCalldataAndSalt() (gas: 790352)\n[PASS] test_duplicateScheduleMalformedTargetsAndUnknownCancellation() (gas: 174487)\n[PASS] test_exactDelayAndGracePeriodEndpoints() (gas: 360909)\n[PASS] test_expiredProposalRequiresCancellationAndFreshDelay() (gas: 277357)\n[PASS] test_failedTargetRetainsProposalAndCanRetryWithoutRescheduling() (gas: 359028)\n[PASS] test_nestedExecutionCannotConsumeAnotherMaturedProposal() (gas: 988353)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 654.68ms (4.68ms CPU time)\n\nRan 14 tests for test/OracleIntegrationEdges.t.sol:OracleIntegrationEdgesTest\n[PASS] testFuzz_decimalNormalizationAcrossFullSupportedRange(uint8,uint8,uint16) (runs: 1000, μ: 278977, ~: 280626)\nLogs:\n  Bound result 1\n  Bound result 5\n  Bound result 974\n\n[PASS] testFuzz_deviationLimitAcceptsEqualityButRejectsOneWeiMore(uint16) (runs: 1000, μ: 358968, ~: 358689)\nLogs:\n  Bound result 5\n\n[PASS] test_agreedCollateralCrashBelowConfiguredFloorStillLiquidates() (gas: 1228861)\n[PASS] test_agreedDebtSpikeAboveConfiguredCeilingStillLiquidates() (gas: 1184351)\n[PASS] test_bankRejectsStaleCollateralButAllowsOracleIndependentRescue() (gas: 1170580)\n[PASS] test_brokenFeedCannotBeReenabledAndFailedEnableRemainsDisabled() (gas: 378133)\n[PASS] test_eachSourceEnforcesItsOwnHeartbeatInclusively() (gas: 347417)\n[PASS] test_extremeAnswerRejectedBeforeNormalizationCanOverflow() (gas: 374253)\n[PASS] test_failedFeedReplacementPreservesOriginalPairAndConfiguration() (gas: 557835)\n[PASS] test_guardianPauseExpiresAndLiquidationResumesWhileBankFrozen() (gas: 2090784)\n[PASS] test_invalidExistingDebtFeedBlocksCrossReserveBorrowAndWithdrawal() (gas: 1324169)\n[PASS] test_oracleAgreementDoesNotAssumeStablecoinPegAndCanTriggerLiquidation() (gas: 1047225)\n[PASS] test_priceBandsRejectOvervaluationButAcceptCollateralCrash() (gas: 500314)\n[PASS] test_zeroRoundMissingStartAndStartAfterUpdateFailOnBothSources() (gas: 769130)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 654.67ms (846.16ms CPU time)\n\nRan 10 tests for test/Audit.t.sol:IndependentAuditTest\n[PASS] testAudit_checkpointFrequencyCannotMateriallyChangeBorrowerDebt() (gas: 22740804)\n[PASS] testAudit_collateralCallbackCannotLiquidateOrBorrow() (gas: 328999)\n[PASS] testAudit_dustFinalizationRejectsHealthyValuableAndInvalidPrice() (gas: 421477)\n[PASS] testAudit_dustTransferFailureRollsBackWriteoff() (gas: 399889)\n[PASS] testAudit_extremeCollapseRecordsBadDebt() (gas: 334702)\n[PASS] testAudit_insolventButPartlyRecoverableDustMustLiquidateFirst() (gas: 253994)\n[PASS] testAudit_minimumDebtAndDustLossDoNotHaltLending() (gas: 1887540)\n[PASS] testAudit_oracleConfigurationRollsBackIfBroken() (gas: 1030185)\n[PASS] testAudit_seizureFlooringToZeroStillSweepsResidue() (gas: 1625768)\n[PASS] testAudit_smallRecoverablePositionCannotTriggerGlobalFreeze() (gas: 971931)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 710.04ms (36.15ms CPU time)\n\nRan 4 tests for test/TokenBoundaryEdges.t.sol:TokenBoundaryEdgesTest\n[PASS] test_everyMutationRejectsCallbackReentryWithTheActualGuard() (gas: 3538890)\n[PASS] test_malformedAndInexactCollateralTransfersRollBackBothDirections() (gas: 3513955)\n[PASS] test_malformedAndInexactDebtTransfersCannotCreateOrEraseDebt() (gas: 5122829)\n[PASS] test_reentryGuardAlsoCoversOutgoingDebtRepaymentAndWithdrawal() (gas: 1363918)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 735.94ms (10.14ms CPU time)\n\nRan 2 tests for test/GovernanceEdges.t.sol:BankAuthorityEdgesTest\n[PASS] testFuzz_unprivilegedAccountCannotChangeAnyRiskControl(address) (runs: 1000, μ: 232091, ~: 232178)\n[PASS] test_guardianRotationRevokesOldAuthorityAndReserveFreezeIsIsolated() (gas: 719172)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 736.33ms (734.97ms CPU time)\n\nRan 11 tests for test/RiskOracle.t.sol:RiskOracleTest\n[PASS] testFuzz_agreementAlwaysUsesSafeSide(uint64,uint64) (runs: 512, μ: 335556, ~: 335333)\nLogs:\n  Bound result 220000000\n  Bound result 9456612\n\n[PASS] test_ageBoundAllowsHeartbeatPlusGrace() (gas: 347389)\n[PASS] test_ageBoundaryAndCircuitBreaker() (gas: 271627)\n[PASS] test_boundsRejectOnlyBorrowerOvervaluation() (gas: 637518)\n[PASS] test_conservativeDirectionAndDecimals() (gas: 326271)\n[PASS] test_depegIsPricedNotAssumedOneDollar() (gas: 194740)\n[PASS] test_feedDecimalsCannotSilentlyChange() (gas: 66330)\n[PASS] test_governanceAndGuardianSeparation() (gas: 265582)\n[PASS] test_guardianPauseExpiresOnceAndGovernanceRearms() (gas: 603417)\n[PASS] test_guardianRotationAndPauseBounds() (gas: 303376)\n[PASS] test_rejectsBadRoundsAndTimes() (gas: 471463)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 752.63ms (669.49ms CPU time)\n\nRan 18 tests for test/AccountingEdges.t.sol:AccountingEdgesTest\n[PASS] testFuzz_minimumDebtUsesReservePriceAndPostBorrowBalance(uint8,uint16) (runs: 1000, μ: 1404703, ~: 1404854)\nLogs:\n  Bound result 5551\n\n[PASS] testFuzz_partialRepaymentMatchesCashAndDebtAcrossDecimals(uint8,uint32,uint256) (runs: 1000, μ: 768861, ~: 768824)\nLogs:\n  Bound result 15\n  Bound result 8753\n\n[PASS] testFuzz_repeatedTinyBorrowRepayCyclesCannotExtractCash(uint8,uint32) (runs: 1000, μ: 5366208, ~: 5365983)\nLogs:\n  Bound result 1\n\n[PASS] test_borrowCapCountsOtherBorrowersAndAccruedInterest() (gas: 1229936)\n[PASS] test_exactBorrowCapacityAndWithdrawalBoundary() (gas: 989939)\n[PASS] test_exactHealthAndCloseFactorBoundaries() (gas: 985739)\n[PASS] test_failedRepaymentDoesNotBurnSharesOrConsumeAllowance() (gas: 609086)\n[PASS] test_interestUpdatesProspectivelyAtGovernanceRateChange() (gas: 766284)\n[PASS] test_kinkAndFullUtilizationRatesThenIdleIndex() (gas: 1050776)\n[PASS] test_liquidationTransferFailureRestoresAllThreeReservesAndWriteoffs() (gas: 1577277)\n[PASS] test_minimumDebtCannotBeSatisfiedByAnotherAccountOrReserve() (gas: 886267)\n[PASS] test_partialRecapitalizationCannotReopenAnyRisk() (gas: 1172206)\n[PASS] test_repayThirdPartyChargesOnlyDebtAndDoesNotTransferOwnership() (gas: 581633)\n[PASS] test_saturatedIndexStopsNewDebtButAllowsFullRepayAndExit() (gas: 910098)\n[PASS] test_supplyCapCountsAllAccountsAndDirectDonationsMintNoClaims() (gas: 500008)\n[PASS] test_unsupportedReserveCannotReachAnyDebtPath() (gas: 210183)\n[PASS] test_zeroAndBankRecipientsAreRejectedOnEveryUserPath() (gas: 643596)\n[PASS] test_zeroAndOversizedInputsFailBeforeMovingFunds() (gas: 508013)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 752.70ms (2.15s CPU time)\n\nRan 1 test for test/Invariants.t.sol:BankInvariantTest\n[PASS]\nBankInvariantTest invariants:\n[PASS] invariant_cashFlowConservation\n[PASS] invariant_collateralConservation\n[PASS] invariant_debtSharesAndAggregateRounding\n BankInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------+-------------------+-------+---------+----------╮\n| Contract    | Selector          | Calls | Reverts | Discards |\n+==============================================================+\n| BankHandler | borrow            | 1150  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | donate            | 1160  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | elapse            | 1151  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | repay             | 1163  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | shockAndLiquidate | 1214  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | supply            | 1183  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | withdraw          | 1171  | 0       | 0        |\n╰-------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1000000000\n  Bound result 2\n  Bound result 142746495\n  Bound result 6982\n  Bound result 2959\n  Bound result 3000000000\n  Bound result 1\n  Bound result 3608\n  Bound result 3\n  Bound result 50\n  Bound result 15663\n  Bound result 6744406167\n  Bound result 3709551614\n  Bound result 9834\n  Bound result 3615567089\n  Bound result 230\n  Bound result 2262736464\n  Bound result 3656\n  Bound result 10592\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 3258\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 7921\n  Bound result 6840\n  Bound result 1692\n  Bound result 41\n  Bound result 2000000000\n  Bound result 500\n  Bound result 1\n  Bound result 2514264337593543950332\n  Bound result 2489\n  Bound result 33\n  Bound result 4155\n  Bound result 61\n  Bound result 6000\n  Bound result 14\n  Bound result 125000\n  Bound result 11452\n  Bound result 16\n  Bound result 200000000\n  Bound result 8580\n  Bound result 11865\n  Bound result 306\n  Bound result 9\n  Bound result 1999000000\n  Bound result 6911\n  Bound result 488306\n  Bound result 51966\n  Bound result 500\n  Bound result 836541142\n  Bound result 358055167\n  Bound result 4592\n  Bound result 499897765969048434\n  Bound result 8738\n  Bound result 22583\n  Bound result 737\n  Bound result 567108\n  Bound result 2846\n  Bound result 26134012\n  Bound result 1162\n  Bound result 509\n  Bound result 1468\n  Bound result 3\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.17s (2.17s CPU time)\n\nRan 3 tests for test/ThreeReserveInvariants.t.sol:ThreeReserveInvariantTest\n[PASS] invariant_allReserveCashClaimsLossesAndRoundingAreAccountedFor() (runs: 256, calls: 24576, reverts: 0)\n\n╭---------------------+--------------+-------+---------+----------╮\n| Contract            | Selector     | Calls | Reverts | Discards |\n+=================================================================+\n| ThreeReserveHandler | borrow       | 2223  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | donate       | 2176  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | elapse       | 2237  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | finalizeDust | 2221  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | liquidate    | 2125  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | recovery     | 2277  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | repay        | 2316  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | shock        | 2281  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | supply       | 2234  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | toggle       | 2216  | 0       | 0        |\n|---------------------+--------------+-------+---------+----------|\n| ThreeReserveHandler | withdraw     | 2270  | 0       | 0        |\n╰---------------------+--------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1600000000000000001\n  Bound result 7132\n  Bound result 5091\n  Bound result 9414\n  Bound result 27\n  Bound result 33834981493357751\n  Bound result 100000000\n  Bound result 1500000000000015384\n  Bound result 100000000\n  Bound result 43950335\n  Bound result 8622\n  Bound result 2370951669136457143214\n  Bound result 115619842587730369001\n  Bound result 1\n  Bound result 21049\n  Bound result 40\n  Bound result 47310442\n  Bound result 18875\n  Bound result 16203\n  Bound result 3601\n  Bound result 1566\n  Bound result 16673\n  Bound result 100000000000000000\n  Bound result 266165023651328153\n  Bound result 3250\n  Bound result 4548\n  Bound result 1\n  Bound result 8620\n  Bound result 114693829688093179205\n  Bound result 3\n  Bound result 159562\n  Bound result 5500000000000100000001\n  Bound result 7819\n  Bound result 100000000\n  Bound result 5500000000000100000001\n  Bound result 54769916\n  Bound result 21088\n  Bound result 1\n  Bound result 710911033\n  Bound result 543950335\n  Bound result 1600000000000000001\n  Bound result 188602695\n  Bound result 100000000\n  Bound result 70474\n  Bound result 863997\n  Bound result 1\n  Bound result 482553024346\n  Bound result 677\n  Bound result 19629\n  Bound result 1\n  Bound result 5001\n  Bound result 1\n  Bound result 1902\n  Bound result 1500000000100000001\n  Bound result 1\n  Bound result 1\n  Bound result 12520\n  Bound result 110128552242177645\n  Bound result 1\n  Bound result 264337593543950335\n  Bound result 1726784\n  Bound result 100000000\n  Bound result 9574312\n  Bound result 1999\n  Bound result 963\n  Bound result 100000000\n  Bound result 1\n\n[PASS] test_handlerReachesLiquidationWriteoffRecoveryAndExit() (gas: 3758034)\nLogs:\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 86400\n  Bound result 1000000000000000000\n  Bound result 61098287\n  Bound result 1\n  Bound result 10000000000000000000\n\n[PASS] test_handlerTracksAccumulatedDustLossAcrossThresholdAndFullRecovery() (gas: 6405673)\nLogs:\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 1000000000000000000000\n  Bound result 100000000\n  Bound result 100000000\n  Bound result 100000000000000000\n  Bound result 99500000\n  Bound result 99500000\n  Bound result 99999999\n  Bound result 1\n  Bound result 500000000000000000\n  Bound result 1000000\n\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 11.99s (11.99s CPU time)\n\nRan 14 test suites in 12.00s (19.82s CPU time): 101 tests passed, 0 failed, 1 skipped (102 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GovernanceTimelock.cancel(bytes32)\",\"GovernanceTimelock.execute(address,bytes,bytes32)\",\"GovernanceTimelock.schedule(address,bytes,bytes32)\",\"IMDBank.accrue(address)\",\"IMDBank.borrow(address,uint256,address)\",\"IMDBank.configureReserve(address,uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.configureRisk(uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.coverBadDebt(address,uint256)\",\"IMDBank.donateLiquidity(address,uint256)\",\"IMDBank.finalizeDust(address)\",\"IMDBank.liquidate(address,address,uint256,uint256,uint256)\",\"IMDBank.repay(address,uint256,address)\",\"IMDBank.setCollateralEnabled(bool)\",\"IMDBank.setFrozen(bool)\",\"IMDBank.setGuardian(address)\",\"IMDBank.setReserveFrozen(address,bool)\",\"IMDBank.supply(uint256,address)\",\"IMDBank.withdraw(uint256,address)\",\"RiskOracle.configure(address,address,address,uint32,uint32,uint16,uint256,uint256,bool)\",\"RiskOracle.setEnabled(address,bool)\",\"RiskOracle.setGuardian(address)\",\"RiskOracle.setGuardianPause(uint256)\"],\"files\":{\".gitignore\":6,\"README.md\":53,\"docs/ARCHITECTURE.md\":85,\"docs/DEPENDENCIES.md\":8,\"docs/DEPLOYMENT.md\":73,\"docs/FRONTEND.md\":82,\"docs/RESEARCH.md\":105,\"docs/SECURITY_REVIEW.md\":122,\"docs/VALIDATION.md\":65,\"docs/evidence/aave-legacy-license.json\":8,\"docs/evidence/aave-legacy-tag.json\":14,\"docs/evidence/aave-origin-commit.json\":21,\"docs/evidence/aave-origin-license.json\":8,\"docs/evidence/bytecode-check.txt\":3,\"docs/evidence/chainlink-mainnet-catalog.json\":150,\"docs/evidence/chainlink-onchain.json\":91,\"docs/evidence/economic-stress.csv\":81,\"docs/evidence/forge-build.txt\":469,\"docs/evidence/forge-fmt.txt\":0,\"docs/evidence/forge-tests.txt\":100,\"docs/evidence/fork-rpc-limitation.json\":8,\"docs/evidence/frontend-abi-check.txt\":1,\"docs/evidence/frontend-fork-initial.json\":46,\"docs/evidence/frontend-fork-round1.json\":43,\"docs/evidence/frontend-fork.json\":41,\"docs/evidence/frontend-unit-tests.txt\":88,\"docs/evidence/imd-admin-holders-onchain.json\":108,\"docs/evidence/imd-blockrazor.json\":53,\"docs/evidence/imd-dexscreener.json\":7,\"docs/evidence/imd-etherscan.json\":5,\"docs/evidence/imd-holders-blockscout.json\":264,\"docs/evidence/imd-onchain.json\":41,\"docs/evidence/imd-sourcify.json\":269,\"docs/evidence/imd-token-blockscout.json\":19,\"docs/evidence/independent-review-checks.json\":47,\"docs/evidence/mainnet-block.json\":20,\"docs/evidence/mainnet-fork-tests.txt\":11,\"docs/evidence/usdc-blockrazor.json\":49,\"docs/evidence/usdc-onchain.json\":71,\"docs/evidence/usdt-onchain.json\":59,\"docs/evidence/weth-onchain.json\":59,\"foundry.toml\":26,\"remappings.txt\":2,\"script/DeployMainnet.s.sol\":51,\"src/GovernanceTimelock.sol\":76,\"src/IMDBank.sol\":645,\"src/RiskOracle.sol\":168,\"src/lib/ExactToken.sol\":58,\"test-fork/Mainnet.t.sol\":144,\"test/ADDED_TEST_COVERAGE.md\":111,\"test/AccountingEdges.t.sol\":413,\"test/Audit.t.sol\":245,\"test/DeploymentRehearsal.t.sol\":67,\"test/GovernanceEdges.t.sol\":238,\"test/GovernanceTimelock.t.sol\":77,\"test/IMDBank.t.sol\":255,\"test/Invariants.t.sol\":151,\"test/OptionalMainnet.t.sol\":137,\"test/OracleIntegrationEdges.t.sol\":391,\"test/RiskControls.t.sol\":110,\"test/RiskOracle.t.sol\":217,\"test/ThreeReserveInvariants.t.sol\":442,\"test/TokenBoundaryEdges.t.sol\":267,\"test/helpers/BankFixture.sol\":78,\"test/helpers/Mocks.sol\":126,\"tools/check_bytecode.py\":22,\"tools/economic_stress.py\":49,\"web/_headers\":11,\"web/abi.js\":34,\"web/app.js\":371,\"web/check-abi.mjs\":2,\"web/config.json\":19,\"web/core.js\":91,\"web/fonts.css\":1,\"web/index.html\":64,\"web/package.json\":9,\"web/styles.css\":2,\"web/tests/abi-compatibility.mjs\":20,\"web/tests/core.test.mjs\":109,\"web/tests/fork-integration.mjs\":162,\"web/vendor/ETHERS-LICENSE.md\":21,\"web/vendor/README.md\":10,\"web/vendor/ethers-6.15.0.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"7f8db58646b1b068377bc70993767061dea354f2e52da4526f1ee9a0520aebb3","verifiedTreeHash":"7ef643cb5b94ea5843549e667e1f777b2d5d5351","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":2606,"exitCode":0,"name":"build","output":"Compiling 36 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.43s\nCompiler run successful!\nwarning[reentrancy-no-eth]: external call can be reentered before `executing` is updated\n   ╭▸ src/GovernanceTimelock.sol:70:44\n   │\n70 │         (bool ok, bytes memory returned) = target.call(data);\n   │                                            ━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:62:51\n   │\n62 │             executing || done[id] || when == 0 || block.timestamp < when\n   │                                                   ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:63:20\n   │\n63 │                 || block.timestamp > when + GRACE_PERIOD\n   │                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/GovernanceTimelock.sol:55:22\n   │\n55 │     function execute(address target, bytes calldata data, bytes32 salt)\n   │                      ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/GovernanceTimelock.sol:73:9\n   │\n73 │         emit Executed(id);\n   │         ━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:104:9\n    │\n104 │         emit FeedConfigured(asset, primary, secondary, collateralSide);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:105:9\n    │\n105 │         emit FeedEnabled(asset, true);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/RiskOracle.sol:117:13\n    │\n117 │             emit FeedEnabled(asset, enabled);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:121:37\n    │\n121 │             f.guardianPausedUntil = uint64(until);\n    │                                     ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint64' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:144:27\n    │\n144 │         if (!f.enabled || block.timestamp < f.guardianPausedUntil) revert Disabled();\n    │                           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:161:78\n    │\n161 │             answer <= 0 || round == 0 || answered < round || updated == 0 || updated > block.timestamp\n    │                                                                              ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:162:57\n    │\n162 │                 || started == 0 || started > updated || block.timestamp - updated > maxAge\n    │                                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:165:13\n    │\n165 │         if (uint256(answer) > 1e36 / 10 ** (18 - decimals_)) revert InvalidPrice();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:166:16\n    │\n166 │         return uint256(answer) * 10 ** (18 - decimals_);\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:22:26\n   │\n22 │         uint8 decimals = IExactERC20(token).decimals();\n   │                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:142:9\n    │\n142 │         address collateral_,\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:143:9\n    │\n143 │         address oracle_,\n    │         ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:162:64\n    │\n162 │             if (asset == collateral_ || assetUnit[asset] != 0) revert InvalidConfiguration();\n    │                                                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:21:37\n   │\n21 │         if (token.code.length == 0) revert InvalidToken();\n   │                                     ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:23:28\n   │\n23 │         if (decimals > 18) revert InvalidToken();\n   │                            ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:209:9\n    │\n209 │         emit Withdrawn(msg.sender, to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:230:9\n    │\n230 │         emit Borrowed(msg.sender, asset, to, amount, shares);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:249:9\n    │\n249 │         emit Repaid(msg.sender, onBehalfOf, asset, paid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:258:9\n    │\n258 │         emit LiquidityDonated(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:270:9\n    │\n270 │         emit BadDebtCovered(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:285:13\n    │\n285 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:529:13\n    │\n529 │             emit BadDebtRecorded(account, asset, amount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:536:17\n    │\n536 │                 emit FrozenStateChanged(asset, true);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:537:17\n    │\n537 │                 emit FrozenStateChanged(address(0), true);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:299:9\n    │\n299 │         emit Liquidated(msg.sender, account, asset, repaid, seized);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:321:9\n    │\n321 │         emit DustFinalized(msg.sender, account, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMDBank.sol:625:18\n    │\n625 │         price_ = IBankOracle(oracle).price(token);\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:630:36\n    │\n630 │         if (assetUnit[asset] == 0) revert UnsupportedAsset();\n    │                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:590:13\n    │\n590 │             revert InvalidConfiguration();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:626:48\n    │\n626 │         if (price_ == 0 || price_ > MAX_PRICE) revert InvalidPrice();\n    │                                                ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:434:9\n    │\n434 │         emit ReserveConfigured(asset, borrowCap, baseRateRay, slope1Ray, slope2Ray, kinkBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:441:51\n    │\n441 │                 if (reserves[assets[i]].lossHalt) revert OutstandingBadDebt();\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:514:28\n    │\n514 │             if (paid != 0) revert InvalidAmount();\n    │                            ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:17:16\n   │\n17 │         return IExactERC20(token).balanceOf(account);\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:583:9\n    │\n583 │         emit Accrued(asset, reserve.index, reserve.cachedRateRay);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:587:45\n    │\n587 │         if (reserve.totalDebtShares == 0 || block.timestamp == reserve.lastAccrual) return reserve.index;\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:589:13\n    │\n589 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:589:51\n    │\n589 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:597:16\n    │\n597 │         while (elapsed != 0) {\n    │                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:598:17\n    │\n598 │             if (elapsed & 1 != 0) accumulated = _rayMulCapped(accumulated, factor);\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:600:17\n    │\n600 │             if (elapsed != 0) factor = _rayMulCapped(factor, factor);\n    │                 ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:51:17\n   │\n51 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:61:17\n   │\n61 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/RiskControls.t.sol:70:21\n   │\n70 │         uint256 t = block.timestamp;\n   │                     ━━━━━━━━━━━━━━━\n71 │         vm.warp(t + 30 days);\n   │         ──────────────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":1499,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/GovernanceTimelock.t.sol:GovernanceTimelockTest\n[PASS] test_cancellationAndAccess() (gas: 210103)\n[PASS] test_delayPermissionlessExecutionAndReplay() (gas: 266474)\n[PASS] test_domainSeparation() (gas: 22790)\n[PASS] test_expiryAndFailureAtomicity() (gas: 200959)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 779.28µs (857.12µs CPU time)\n\nRan 2 tests for test/DeploymentRehearsal.t.sol:DeploymentRehearsalTest\n[PASS] test_explicitRolesFromFactoryLikeCallerAndTimelockedRisk() (gas: 12475338)\n[PASS] test_wrongChainEOAAndSharedRolesRejected() (gas: 5798019)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.57ms (1.65ms CPU time)\n\nRan 7 tests for test/RiskControls.t.sol:RiskControlsTest\n[PASS] test_capReductionCannotBlockRepayment() (gas: 675227)\n[PASS] test_hardRiskBoundsCannotBeBypassedByGovernor() (gas: 154202)\n[PASS] test_liquidityExhaustionRevertsWithoutMintingDebt() (gas: 315876)\n[PASS] test_lossRecognitionRecapitalizationAndRestart() (gas: 1174412)\n[PASS] test_multiReserveDebtWrittenOffAfterCollateralExhausted() (gas: 1309899)\n[PASS] test_permissionlessLiquidationFitsGasBudget() (gas: 606658)\n[PASS] test_tokenBalanceDonationCannotRetroactivelyChangeRate() (gas: 492373)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 4.64ms (5.07ms CPU time)\n\nRan 11 tests for test/RiskOracle.t.sol:RiskOracleTest\n[PASS] testFuzz_agreementAlwaysUsesSafeSide(uint64,uint64) (runs: 512, μ: 335540, ~: 335333)\nLogs:\n  Bound result 729121766\n  Bound result 59404732\n\n[PASS] test_ageBoundAllowsHeartbeatPlusGrace() (gas: 347389)\n[PASS] test_ageBoundaryAndCircuitBreaker() (gas: 271627)\n[PASS] test_boundsRejectOnlyBorrowerOvervaluation() (gas: 637518)\n[PASS] test_conservativeDirectionAndDecimals() (gas: 326271)\n[PASS] test_depegIsPricedNotAssumedOneDollar() (gas: 194740)\n[PASS] test_feedDecimalsCannotSilentlyChange() (gas: 66330)\n[PASS] test_governanceAndGuardianSeparation() (gas: 265582)\n[PASS] test_guardianPauseExpiresOnceAndGovernanceRearms() (gas: 603417)\n[PASS] test_guardianRotationAndPauseBounds() (gas: 303376)\n[PASS] test_rejectsBadRoundsAndTimes() (gas: 471463)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 30.69ms (33.60ms CPU time)\n\nRan 10 tests for test/Audit.t.sol:IndependentAuditTest\n[PASS] testAudit_checkpointFrequencyCannotMateriallyChangeBorrowerDebt() (gas: 22740804)\n[PASS] testAudit_collateralCallbackCannotLiquidateOrBorrow() (gas: 328999)\n[PASS] testAudit_dustFinalizationRejectsHealthyValuableAndInvalidPrice() (gas: 421477)\n[PASS] testAudit_dustTransferFailureRollsBackWriteoff() (gas: 399889)\n[PASS] testAudit_extremeCollapseRecordsBadDebt() (gas: 334702)\n[PASS] testAudit_insolventButPartlyRecoverableDustMustLiquidateFirst() (gas: 253994)\n[PASS] testAudit_minimumDebtAndDustLossDoNotHaltLending() (gas: 1887540)\n[PASS] testAudit_oracleConfigurationRollsBackIfBroken() (gas: 1030185)\n[PASS] testAudit_seizureFlooringToZeroStillSweepsResidue() (gas: 1625768)\n[PASS] testAudit_smallRecoverablePositionCannotTriggerGlobalFreeze() (gas: 971931)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 36.75ms (23.59ms CPU time)\n\nRan 17 tests for test/IMDBank.t.sol:IMDBankTest\n[PASS] testFuzz_borrowRepayRoundingCannotCreateAssets(uint64,uint32) (runs: 512, μ: 567754, ~: 566788)\nLogs:\n  Bound result 2499003501\n  Bound result 9282\n\n[PASS] testFuzz_roundTripNoCollateralGain(uint96) (runs: 512, μ: 222867, ~: 222727)\nLogs:\n  Bound result 3134672720056222550121\n\n[PASS] test_completeLifecycleThreeReserves() (gas: 1433109)\n[PASS] test_flashSupplyBorrowWithdrawCannotEscapeDebt() (gas: 483863)\n[PASS] test_guardianCannotUnfreezeOrChangeRisk() (gas: 759157)\n[PASS] test_initialDeploymentCannotTakeRisk() (gas: 125584)\n[PASS] test_liquidationSlippageAndExpiryProtectPayer() (gas: 575358)\n[PASS] test_noReturnTokensWorkAndFalseReturnRejected() (gas: 621499)\n[PASS] test_oracleOutageStillAllowsRepaymentAndDebtFreeExit() (gas: 764020)\n[PASS] test_partialRepayNeverReducesDebtMoreThanPayment() (gas: 568911)\n[PASS] test_priceDropLiquidationAndCloseFactor() (gas: 868219)\n[PASS] test_reentrancyTransferCallbackCannotMintUnbackedReceipts() (gas: 313788)\n[PASS] test_rejectsOverborrowWithdrawalAndCollateralDisable() (gas: 739314)\n[PASS] test_safeHealthRejectsLiquidation() (gas: 489468)\n[PASS] test_stablecoinDepegAndWethSpikeChangeDebtValue() (gas: 782234)\n[PASS] test_supplyOnBehalfDoesNotEnableOthersCollateral() (gas: 173849)\n[PASS] test_taxedTransfersFailAtomicallyInAndOut() (gas: 773562)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 45.35ms (76.56ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BankInvariantTest\n[PASS]\nBankInvariantTest invariants:\n[PASS] invariant_cashFlowConservation\n[PASS] invariant_collateralConservation\n[PASS] invariant_debtSharesAndAggregateRounding\n BankInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------+-------------------+-------+---------+----------╮\n| Contract    | Selector          | Calls | Reverts | Discards |\n+==============================================================+\n| BankHandler | borrow            | 1181  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | donate            | 1136  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | elapse            | 1184  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | repay             | 1209  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | shockAndLiquidate | 1162  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | supply            | 1187  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | withdraw          | 1133  | 0       | 0        |\n╰-------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 1170\n  Bound result 5524306121\n  Bound result 1\n  Bound result 4836\n  Bound result 7107\n  Bound result 1746632\n  Bound result 17\n  Bound result 2866\n  Bound result 6127\n  Bound result 1\n  Bound result 1247\n  Bound result 4181\n  Bound result 10578\n  Bound result 11157\n  Bound result 1200000000000000000\n  Bound result 1050000000000000000\n  Bound result 2518958\n  Bound result 4122\n  Bound result 11762\n  Bound result 2\n  Bound result 10433\n  Bound result 4554\n  Bound result 900000000\n  Bound result 9743\n  Bound result 5184000\n  Bound result 13\n  Bound result 113\n  Bound result 5632\n  Bound result 155918\n  Bound result 1498\n  Bound result 10000001\n  Bound result 2144\n  Bound result 47656992314257953\n  Bound result 60\n  Bound result 1432226\n  Bound result 100000000000\n  Bound result 73709551614\n  Bound result 10000000000\n  Bound result 1000000000\n  Bound result 9558\n  Bound result 1\n  Bound result 376403338\n  Bound result 10000000000\n  Bound result 7390531921\n  Bound result 1221\n  Bound result 10224\n  Bound result 6495\n  Bound result 8011808\n  Bound result 1000000000\n  Bound result 99\n  Bound result 6265065473\n  Bound result 2400000000\n  Bound result 10215\n  Bound result 5414710698499723441283\n  Bound result 11539\n  Bound result 3548\n  Bound result 101000000\n  Bound result 864000\n  Bound result 10420\n  Bound result 8771\n  Bound result 1879341\n  Bound result 70428905758169907\n  Bound result 132273396057273866585\n  Bound result 3701074320\n  Bound result 659918\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.39s (1.39s CPU time)\n\nRan 7 test suites in 1.40s (1.51s CPU time): 52 tests passed, 0 failed, 0 skipped (52 total tests)\n","passed":true},{"durationMs":81,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GovernanceTimelock.cancel(bytes32)\",\"GovernanceTimelock.execute(address,bytes,bytes32)\",\"GovernanceTimelock.schedule(address,bytes,bytes32)\",\"IMDBank.accrue(address)\",\"IMDBank.borrow(address,uint256,address)\",\"IMDBank.configureReserve(address,uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.configureRisk(uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.coverBadDebt(address,uint256)\",\"IMDBank.donateLiquidity(address,uint256)\",\"IMDBank.finalizeDust(address)\",\"IMDBank.liquidate(address,address,uint256,uint256,uint256)\",\"IMDBank.repay(address,uint256,address)\",\"IMDBank.setCollateralEnabled(bool)\",\"IMDBank.setFrozen(bool)\",\"IMDBank.setGuardian(address)\",\"IMDBank.setReserveFrozen(address,bool)\",\"IMDBank.supply(uint256,address)\",\"IMDBank.withdraw(uint256,address)\",\"RiskOracle.configure(address,address,address,uint32,uint32,uint16,uint256,uint256,bool)\",\"RiskOracle.setEnabled(address,bool)\",\"RiskOracle.setGuardian(address)\",\"RiskOracle.setGuardianPause(uint256)\"],\"files\":{\".gitignore\":6,\"README.md\":53,\"docs/ARCHITECTURE.md\":85,\"docs/DEPENDENCIES.md\":8,\"docs/DEPLOYMENT.md\":73,\"docs/FRONTEND.md\":82,\"docs/RESEARCH.md\":105,\"docs/SECURITY_REVIEW.md\":122,\"docs/VALIDATION.md\":65,\"docs/evidence/aave-legacy-license.json\":8,\"docs/evidence/aave-legacy-tag.json\":14,\"docs/evidence/aave-origin-commit.json\":21,\"docs/evidence/aave-origin-license.json\":8,\"docs/evidence/bytecode-check.txt\":3,\"docs/evidence/chainlink-mainnet-catalog.json\":150,\"docs/evidence/chainlink-onchain.json\":91,\"docs/evidence/economic-stress.csv\":81,\"docs/evidence/forge-build.txt\":469,\"docs/evidence/forge-fmt.txt\":0,\"docs/evidence/forge-tests.txt\":100,\"docs/evidence/fork-rpc-limitation.json\":8,\"docs/evidence/frontend-abi-check.txt\":1,\"docs/evidence/frontend-fork-initial.json\":46,\"docs/evidence/frontend-fork-round1.json\":43,\"docs/evidence/frontend-fork.json\":41,\"docs/evidence/frontend-unit-tests.txt\":88,\"docs/evidence/imd-admin-holders-onchain.json\":108,\"docs/evidence/imd-blockrazor.json\":53,\"docs/evidence/imd-dexscreener.json\":7,\"docs/evidence/imd-etherscan.json\":5,\"docs/evidence/imd-holders-blockscout.json\":264,\"docs/evidence/imd-onchain.json\":41,\"docs/evidence/imd-sourcify.json\":269,\"docs/evidence/imd-token-blockscout.json\":19,\"docs/evidence/independent-review-checks.json\":47,\"docs/evidence/mainnet-block.json\":20,\"docs/evidence/mainnet-fork-tests.txt\":11,\"docs/evidence/usdc-blockrazor.json\":49,\"docs/evidence/usdc-onchain.json\":71,\"docs/evidence/usdt-onchain.json\":59,\"docs/evidence/weth-onchain.json\":59,\"foundry.toml\":26,\"remappings.txt\":2,\"script/DeployMainnet.s.sol\":51,\"src/GovernanceTimelock.sol\":76,\"src/IMDBank.sol\":645,\"src/RiskOracle.sol\":168,\"src/lib/ExactToken.sol\":58,\"test-fork/Mainnet.t.sol\":144,\"test/Audit.t.sol\":245,\"test/DeploymentRehearsal.t.sol\":67,\"test/GovernanceTimelock.t.sol\":77,\"test/IMDBank.t.sol\":255,\"test/Invariants.t.sol\":151,\"test/RiskControls.t.sol\":110,\"test/RiskOracle.t.sol\":217,\"test/helpers/BankFixture.sol\":78,\"test/helpers/Mocks.sol\":126,\"tools/check_bytecode.py\":22,\"tools/economic_stress.py\":49,\"web/_headers\":11,\"web/abi.js\":34,\"web/app.js\":371,\"web/check-abi.mjs\":2,\"web/config.json\":19,\"web/core.js\":91,\"web/fonts.css\":1,\"web/index.html\":64,\"web/package.json\":9,\"web/styles.css\":2,\"web/tests/abi-compatibility.mjs\":20,\"web/tests/core.test.mjs\":109,\"web/tests/fork-integration.mjs\":162,\"web/vendor/ETHERS-LICENSE.md\":21,\"web/vendor/README.md\":10,\"web/vendor/ethers-6.15.0.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1726,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/IMDBank.sol:586: IMDBank._previewIndex(IMDBank.Reserve) (src/IMDBank.sol#586-603) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/GovernanceTimelock.sol:48: GovernanceTimelock.cancel(bytes32) (src/GovernanceTimelock.sol#48-53) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:234: Reentrancy in IMDBank.repay(address,uint256,address) (src/IMDBank.sol#234-250):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:278: Reentrancy in IMDBank.liquidate(address,address,uint256,uint256,uint256) (src/IMDBank.sol#278-300):\n[medium/medium] reentrancy-no-eth at src/GovernanceTimelock.sol:55: Reentrancy in GovernanceTimelock.execute(address,bytes,bytes32) (src/GovernanceTimelock.sol#55-75):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:252: Reentrancy in IMDBank.donateLiquidity(address,uint256) (src/IMDBank.sol#252-259):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:261: Reentrancy in IMDBank.coverBadDebt(address,uint256) (src/IMDBank.sol#261-271):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:212: Reentrancy in IMDBank.borrow(address,uint256,address) (src/IMDBank.sol#212-231):\n[medium/medium] uninitialized-local at src/IMDBank.sol:469: IMDBank._liquidationQuote(address,address,uint256).quote (src/IMDBank.sol#469) is a local variable never initialized\n[low/medium] missing-zero-check at src/GovernanceTimelock.sol:55: GovernanceTimelock.execute(address,bytes,bytes32).target (src/GovernanceTimelock.sol#55) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMDBank.sol:142: IMDBank.constructor(address,address,address,address,address,address,address).collateral_ (src/IMDBank.sol#142) lacks a zero-check on :\n[low/medium] reentrancy-events at src/GovernanceTimelock.sol:55: Reentrancy in GovernanceTimelock.execute(address,bytes,bytes32) (src/GovernanceTimelock.sol#55-75):\n[low/medium] timestamp at src/GovernanceTimelock.sol:55: GovernanceTimelock.execute(address,bytes,bytes32) (src/GovernanceTimelock.sol#55-75) uses timestamp for comparisons\n[low/medium] timestamp at src/RiskOracle.sol:142: RiskOracle.price(address) (src/RiskOracle.sol#142-155) uses timestamp for comparisons\n[low/medium] timestamp at src/IMDBank.sol:278: IMDBank.liquidate(address,address,uint256,uint256,uint256) (src/IMDBank.sol#278-300) uses timestamp for comparisons\n[low/medium] timestamp at src/RiskOracle.sol:157: RiskOracle._read(address,uint8,uint32) (src/RiskOracle.sol#157-167) uses timestamp for comparisons\n[low/medium] timestamp at src/IMDBank.sol:586: IMDBank._previewIndex(IMDBank.Reserve) (src/IMDBank.sol#586-603) uses timestamp for comparisons\n[low/medium] timestamp at src/GovernanceTimelock.sol:48: GovernanceTimelock.cancel(bytes32) (src/GovernanceTimelock.sol#48-53) uses timestamp for comparisons","passed":true},{"durationMs":429,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/GovernanceTimelock.sol:70: Reentrancy: State change after external call (3 places)\n[high] unsafe-casting at src/RiskOracle.sol:121: Unsafe Casting of integers\n[low] costly-loop at src/IMDBank.sol:520: Costly operations inside loop (2 places)\n[low] large-numeric-literal at src/IMDBank.sol:22: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/IMDBank.sol:160: Literal Instead of Constant (21 places)\n[low] local-variable-shadowing at src/IMDBank.sol:65: Local Variable Shadows State Variable\n[low] missing-inheritance at src/RiskOracle.sol:18: Missing Inheritance\n[low] require-revert-in-loop at src/IMDBank.sol:353: Loop Contains `require`/`revert` (5 places)\n[low] state-variable-could-be-immutable at src/IMDBank.sol:40: State Variable Could Be Immutable\n[low] unchecked-return at src/IMDBank.sol:286: Unchecked Return (3 places)\n[low] uninitialized-local-variable at src/IMDBank.sol:160: Uninitialized Local Variable (7 places)\n[low] unused-public-function at src/IMDBank.sol:324: Public Function Not Used Internally","passed":true},{"durationMs":1539,"exitCode":0,"name":"proof dce36aad6ad4","output":"Compiling 29 files with Solc 0.8.26\nSolc 0.8.26 finished in 986.05ms\nCompiler run successful!\n\nRan 1 test for test/imd-proof-56fcbd50/Proof_dce36aad6ad4.t.sol:OracleBandBlocksLiquidationTest\n[PASS] test_collateralCrashBelowFloorMustStillBeLiquidatable() (gas: 341663)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.47ms (247.54µs CPU time)\n\nRan 1 test suite in 2.69ms (1.47ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1264,"exitCode":0,"name":"proof 43610f6cbada","output":"2026-10-06T16:10:16.063379Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-VV0jee/repo/test/imd-proof-56fcbd50/Proof_dce36aad6ad4.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 735.89ms\nCompiler run successful!\n\nRan 1 test for test/imd-proof-25123d3f/Proof_43610f6cbada.t.sol:DustPositionGlobalFreezeTest\n[PASS] test_oneUnitDebtCannotFreezeTheWholeBank() (gas: 331377)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 882.96µs (146.38µs CPU time)\n\nRan 1 test suite in 4.06ms (882.96µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1403,"exitCode":0,"name":"proof 065261cb7129","output":"2026-10-06T16:10:17.356372Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-VV0jee/repo/test/imd-proof-25123d3f/Proof_43610f6cbada.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 851.89ms\nCompiler run successful!\n\nRan 1 test for test/imd-proof-711e5c8d/Proof_065261cb7129.t.sol:GuardianVetoTest\n[PASS] test_emergencyKeyCannotPermanentlyBlockLiquidations() (gas: 1059876)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.18ms (695.92µs CPU time)\n\nRan 1 test suite in 2.87ms (2.18ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":1374,"exitCode":0,"name":"proof 1777553042b2","output":"2026-10-06T16:10:18.778220Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-VV0jee/repo/test/imd-proof-711e5c8d/Proof_065261cb7129.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 838.51ms\nCompiler run successful!\n\nRan 1 test for test/imd-proof-b1717996/Proof_1777553042b2.t.sol:HeartbeatWindowProofTest\n[PASS] test_stablecoinHeartbeatWindowMustNotBlockLiquidation() (gas: 378774)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.78ms (310.83µs CPU time)\n\nRan 1 test suite in 2.73ms (1.78ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"859d72692eec08f9913f88c9a85f730008c30531b947a3731409444751a95ed3","verifiedTreeHash":"a79eded935b7c3e0c0c3d3ac65a4e025e95126c3","verifierVersion":"0.1.0+e6140b7a"},{"checks":[{"durationMs":2752,"exitCode":0,"name":"build","output":"Compiling 36 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.58s\nCompiler run successful!\nwarning[reentrancy-no-eth]: external call can be reentered before `executing` is updated\n   ╭▸ src/GovernanceTimelock.sol:70:44\n   │\n70 │         (bool ok, bytes memory returned) = target.call(data);\n   │                                            ━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:88:9\n   │\n88 │         emit FeedConfigured(asset, primary, secondary, collateralSide);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:89:9\n   │\n89 │         emit FeedEnabled(asset, true);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:62:51\n   │\n62 │             executing || done[id] || when == 0 || block.timestamp < when\n   │                                                   ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/GovernanceTimelock.sol:63:20\n   │\n63 │                 || block.timestamp > when + GRACE_PERIOD\n   │                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/GovernanceTimelock.sol:55:22\n   │\n55 │     function execute(address target, bytes calldata data, bytes32 salt)\n   │                      ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/GovernanceTimelock.sol:73:9\n   │\n73 │         emit Executed(id);\n   │         ━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/RiskOracle.sol:97:9\n   │\n97 │         emit FeedEnabled(asset, enabled);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:119:78\n    │\n119 │             answer <= 0 || round == 0 || answered < round || updated == 0 || updated > block.timestamp\n    │                                                                              ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/RiskOracle.sol:120:57\n    │\n120 │                 || started == 0 || started > updated || block.timestamp - updated > maxAge\n    │                                                         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:123:13\n    │\n123 │         if (uint256(answer) > 1e36 / 10 ** (18 - decimals_)) revert InvalidPrice();\n    │             ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/RiskOracle.sol:124:16\n    │\n124 │         return uint256(answer) * 10 ** (18 - decimals_);\n    │                ━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:22:26\n   │\n22 │         uint8 decimals = IExactERC20(token).decimals();\n   │                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:135:9\n    │\n135 │         address oracle_,\n    │         ━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMDBank.sol:134:9\n    │\n134 │         address collateral_,\n    │         ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:154:64\n    │\n154 │             if (asset == collateral_ || assetUnit[asset] != 0) revert InvalidConfiguration();\n    │                                                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:21:37\n   │\n21 │         if (token.code.length == 0) revert InvalidToken();\n   │                                     ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n   ╭▸ src/lib/ExactToken.sol:23:28\n   │\n23 │         if (decimals > 18) revert InvalidToken();\n   │                            ━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:200:9\n    │\n200 │         emit Withdrawn(msg.sender, to, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:218:9\n    │\n218 │         emit Borrowed(msg.sender, asset, to, amount, shares);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:237:9\n    │\n237 │         emit Repaid(msg.sender, onBehalfOf, asset, paid, burned);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:246:9\n    │\n246 │         emit LiquidityDonated(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:257:9\n    │\n257 │         emit BadDebtCovered(msg.sender, asset, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:272:13\n    │\n272 │         if (block.timestamp > deadline) revert Expired();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:512:13\n    │\n512 │             emit BadDebtRecorded(account, asset, amount);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:513:13\n    │\n513 │             emit FrozenStateChanged(asset, true);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:514:13\n    │\n514 │             emit FrozenStateChanged(address(0), true);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:286:9\n    │\n286 │         emit Liquidated(msg.sender, account, asset, repaid, seized);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:308:9\n    │\n308 │         emit DustFinalized(msg.sender, account, amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMDBank.sol:597:18\n    │\n597 │         price_ = IBankOracle(oracle).price(token);\n    │                  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:602:36\n    │\n602 │         if (assetUnit[asset] == 0) revert UnsupportedAsset();\n    │                                    ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:562:13\n    │\n562 │             revert InvalidConfiguration();\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:598:48\n    │\n598 │         if (price_ == 0 || price_ > MAX_PRICE) revert InvalidPrice();\n    │                                                ━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:416:9\n    │\n416 │         emit ReserveConfigured(asset, borrowCap, baseRateRay, slope1Ray, slope2Ray, kinkBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:423:55\n    │\n423 │                 if (reserves[assets[i]].badDebt != 0) revert OutstandingBadDebt();\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMDBank.sol:495:28\n    │\n495 │             if (paid != 0) revert InvalidAmount();\n    │                            ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/lib/ExactToken.sol:17:16\n   │\n17 │         return IExactERC20(token).balanceOf(account);\n   │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMDBank.sol:555:9\n    │\n555 │         emit Accrued(asset, reserve.index, reserve.cachedRateRay);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:559:45\n    │\n559 │         if (reserve.totalDebtShares == 0 || block.timestamp == reserve.lastAccrual) return reserve.index;\n    │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:561:13\n    │\n561 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:561:51\n    │\n561 │         if (block.timestamp > type(uint64).max || block.timestamp < reserve.lastAccrual) {\n    │                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:569:16\n    │\n569 │         while (elapsed != 0) {\n    │                ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:570:17\n    │\n570 │             if (elapsed & 1 != 0) accumulated = _rayMulCapped(accumulated, factor);\n    │                 ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/IMDBank.sol:572:17\n    │\n572 │             if (elapsed != 0) factor = _rayMulCapped(factor, factor);\n    │                 ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:51:17\n   │\n51 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/GovernanceTimelock.t.sol:61:17\n   │\n61 │         vm.warp(block.timestamp + 2 days);\n   │         ────────━━━━━━━━━━━━━━━────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.timestamp` may be reused across `vm.warp`\n   ╭▸ test/RiskControls.t.sol:61:21\n   │\n61 │         uint256 t = block.timestamp;\n   │                     ━━━━━━━━━━━━━━━\n62 │         vm.warp(t + 30 days);\n   │         ──────────────────── `vm.warp` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockTimestamp()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":1459,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/GovernanceTimelock.t.sol:GovernanceTimelockTest\n[PASS] test_cancellationAndAccess() (gas: 210103)\n[PASS] test_delayPermissionlessExecutionAndReplay() (gas: 266474)\n[PASS] test_domainSeparation() (gas: 22790)\n[PASS] test_expiryAndFailureAtomicity() (gas: 200959)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 794.24µs (631.29µs CPU time)\n\nRan 2 tests for test/DeploymentRehearsal.t.sol:DeploymentRehearsalTest\n[PASS] test_explicitRolesFromFactoryLikeCallerAndTimelockedRisk() (gas: 11710046)\n[PASS] test_wrongChainAndEOARolesRejected() (gas: 5110289)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 967.04µs (897.56µs CPU time)\n\nRan 7 tests for test/RiskControls.t.sol:RiskControlsTest\n[PASS] test_capReductionCannotBlockRepayment() (gas: 676247)\n[PASS] test_hardRiskBoundsCannotBeBypassedByGovernor() (gas: 154047)\n[PASS] test_liquidityExhaustionRevertsWithoutMintingDebt() (gas: 318099)\n[PASS] test_lossRecognitionRecapitalizationAndRestart() (gas: 990244)\n[PASS] test_multiReserveDebtWrittenOffAfterCollateralExhausted() (gas: 1299263)\n[PASS] test_permissionlessLiquidationFitsGasBudget() (gas: 605718)\n[PASS] test_tokenBalanceDonationCannotRetroactivelyChangeRate() (gas: 491413)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 2.56ms (3.90ms CPU time)\n\nRan 9 tests for test/Audit.t.sol:IndependentAuditTest\n[PASS] testAudit_actualTinyLossTriggersGlobalSafetyHalt() (gas: 771740)\n[PASS] testAudit_checkpointFrequencyCannotMateriallyChangeBorrowerDebt() (gas: 22765073)\n[PASS] testAudit_collateralCallbackCannotLiquidateOrBorrow() (gas: 328890)\n[PASS] testAudit_dustFinalizationRejectsHealthyValuableAndInvalidPrice() (gas: 421454)\n[PASS] testAudit_dustTransferFailureRollsBackWriteoff() (gas: 397286)\n[PASS] testAudit_extremeCollapseRecordsBadDebt() (gas: 332121)\n[PASS] testAudit_insolventButPartlyRecoverableDustMustLiquidateFirst() (gas: 254035)\n[PASS] testAudit_oracleConfigurationRollsBackIfBroken() (gas: 1029291)\n[PASS] testAudit_smallRecoverablePositionCannotTriggerGlobalFreeze() (gas: 970961)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 21.66ms (23.55ms CPU time)\n\nRan 7 tests for test/RiskOracle.t.sol:RiskOracleTest\n[PASS] testFuzz_agreementAlwaysUsesSafeSide(uint64,uint64) (runs: 512, μ: 334597, ~: 334401)\nLogs:\n  Bound result 94523616\n  Bound result 4652937\n\n[PASS] test_ageBoundaryAndCircuitBreaker() (gas: 271330)\n[PASS] test_conservativeDirectionAndDecimals() (gas: 325304)\n[PASS] test_depegIsPricedNotAssumedOneDollar() (gas: 194230)\n[PASS] test_feedDecimalsCannotSilentlyChange() (gas: 66234)\n[PASS] test_governanceAndGuardianSeparation() (gas: 251828)\n[PASS] test_rejectsBadRoundsAndTimes() (gas: 470682)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 37.03ms (37.82ms CPU time)\n\nRan 17 tests for test/IMDBank.t.sol:IMDBankTest\n[PASS] testFuzz_borrowRepayRoundingCannotCreateAssets(uint64,uint32) (runs: 512, μ: 566336, ~: 565511)\nLogs:\n  Bound result 20026\n  Bound result 6392000\n\n[PASS] testFuzz_roundTripNoCollateralGain(uint96) (runs: 512, μ: 222896, ~: 222716)\nLogs:\n  Bound result 7026243574000155598400\n\n[PASS] test_completeLifecycleThreeReserves() (gas: 1429741)\n[PASS] test_flashSupplyBorrowWithdrawCannotEscapeDebt() (gas: 482815)\n[PASS] test_guardianCannotUnfreezeOrChangeRisk() (gas: 758192)\n[PASS] test_initialDeploymentCannotTakeRisk() (gas: 125602)\n[PASS] test_liquidationSlippageAndExpiryProtectPayer() (gas: 574550)\n[PASS] test_noReturnTokensWorkAndFalseReturnRejected() (gas: 620428)\n[PASS] test_oracleOutageStillAllowsRepaymentAndDebtFreeExit() (gas: 764952)\n[PASS] test_partialRepayNeverReducesDebtMoreThanPayment() (gas: 567950)\n[PASS] test_priceDropLiquidationAndCloseFactor() (gas: 867321)\n[PASS] test_reentrancyTransferCallbackCannotMintUnbackedReceipts() (gas: 313832)\n[PASS] test_rejectsOverborrowWithdrawalAndCollateralDisable() (gas: 740270)\n[PASS] test_safeHealthRejectsLiquidation() (gas: 488508)\n[PASS] test_stablecoinDepegAndWethSpikeChangeDebtValue() (gas: 780072)\n[PASS] test_supplyOnBehalfDoesNotEnableOthersCollateral() (gas: 173849)\n[PASS] test_taxedTransfersFailAtomicallyInAndOut() (gas: 772536)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 50.13ms (84.95ms CPU time)\n\nRan 1 test for test/Invariants.t.sol:BankInvariantTest\n[PASS]\nBankInvariantTest invariants:\n[PASS] invariant_cashFlowConservation\n[PASS] invariant_collateralConservation\n[PASS] invariant_debtSharesAndAggregateRounding\n BankInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------+-------------------+-------+---------+----------╮\n| Contract    | Selector          | Calls | Reverts | Discards |\n+==============================================================+\n| BankHandler | borrow            | 1210  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | donate            | 1110  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | elapse            | 1182  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | repay             | 1170  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | shockAndLiquidate | 1146  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | supply            | 1178  | 0       | 0        |\n|-------------+-------------------+-------+---------+----------|\n| BankHandler | withdraw          | 1196  | 0       | 0        |\n╰-------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 655518\n  Bound result 427\n  Bound result 3697\n  Bound result 5498\n  Bound result 0\n  Bound result 107477464\n  Bound result 1046363171\n  Bound result 36070670474\n  Bound result 7696\n  Bound result 1046363172\n  Bound result 1740\n  Bound result 67\n  Bound result 2241640\n  Bound result 1000000000\n  Bound result 4505\n  Bound result 2519\n  Bound result 202047189\n  Bound result 21636\n  Bound result 2\n  Bound result 86400\n  Bound result 1\n  Bound result 557\n  Bound result 7898\n  Bound result 104242358449\n  Bound result 366289423\n  Bound result 2936\n  Bound result 204291905\n  Bound result 8000\n  Bound result 1501\n  Bound result 1765147\n  Bound result 1426\n  Bound result 2098\n  Bound result 1902\n  Bound result 3244\n  Bound result 9500\n  Bound result 61510\n  Bound result 2500\n  Bound result 3204\n  Bound result 660694\n  Bound result 9870918448\n  Bound result 41794\n  Bound result 53335\n  Bound result 3057\n  Bound result 11272\n  Bound result 520506160\n  Bound result 3748\n  Bound result 800\n  Bound result 68696826956\n  Bound result 10000000000000000000000\n  Bound result 6709\n  Bound result 2176\n  Bound result 2400\n  Bound result 9502\n  Bound result 86400\n  Bound result 55462258\n  Bound result 18\n  Bound result 2800415861803218557\n  Bound result 1462\n  Bound result 1671150390\n  Bound result 8281\n  Bound result 480\n  Bound result 9999999999999999999573\n  Bound result 1789868205667190844584\n  Bound result 51966\n  Bound result 6364\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.36s (1.36s CPU time)\n\nRan 7 test suites in 1.36s (1.47s CPU time): 47 tests passed, 0 failed, 0 skipped (47 total tests)\n","passed":true},{"durationMs":57,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"GovernanceTimelock.cancel(bytes32)\",\"GovernanceTimelock.execute(address,bytes,bytes32)\",\"GovernanceTimelock.schedule(address,bytes,bytes32)\",\"IMDBank.accrue(address)\",\"IMDBank.borrow(address,uint256,address)\",\"IMDBank.configureReserve(address,uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.configureRisk(uint256,uint256,uint256,uint256,uint256)\",\"IMDBank.coverBadDebt(address,uint256)\",\"IMDBank.donateLiquidity(address,uint256)\",\"IMDBank.finalizeDust(address)\",\"IMDBank.liquidate(address,address,uint256,uint256,uint256)\",\"IMDBank.repay(address,uint256,address)\",\"IMDBank.setCollateralEnabled(bool)\",\"IMDBank.setFrozen(bool)\",\"IMDBank.setGuardian(address)\",\"IMDBank.setReserveFrozen(address,bool)\",\"IMDBank.supply(uint256,address)\",\"IMDBank.withdraw(uint256,address)\",\"RiskOracle.configure(address,address,address,uint32,uint32,uint16,uint256,uint256,bool)\",\"RiskOracle.setEnabled(address,bool)\"],\"files\":{\".gitignore\":6,\"README.md\":53,\"docs/ARCHITECTURE.md\":77,\"docs/DEPENDENCIES.md\":8,\"docs/DEPLOYMENT.md\":72,\"docs/FRONTEND.md\":80,\"docs/RESEARCH.md\":105,\"docs/SECURITY_REVIEW.md\":85,\"docs/VALIDATION.md\":64,\"docs/evidence/aave-legacy-license.json\":8,\"docs/evidence/aave-legacy-tag.json\":14,\"docs/evidence/aave-origin-commit.json\":21,\"docs/evidence/aave-origin-license.json\":8,\"docs/evidence/bytecode-check.txt\":3,\"docs/evidence/chainlink-mainnet-catalog.json\":150,\"docs/evidence/chainlink-onchain.json\":91,\"docs/evidence/economic-stress.csv\":81,\"docs/evidence/forge-build.txt\":396,\"docs/evidence/forge-fmt.txt\":0,\"docs/evidence/forge-tests.txt\":95,\"docs/evidence/fork-rpc-limitation.json\":8,\"docs/evidence/frontend-abi-check.txt\":1,\"docs/evidence/frontend-fork-initial.json\":46,\"docs/evidence/frontend-fork.json\":43,\"docs/evidence/frontend-unit-tests.txt\":20,\"docs/evidence/imd-admin-holders-onchain.json\":108,\"docs/evidence/imd-blockrazor.json\":53,\"docs/evidence/imd-dexscreener.json\":7,\"docs/evidence/imd-etherscan.json\":5,\"docs/evidence/imd-holders-blockscout.json\":264,\"docs/evidence/imd-onchain.json\":41,\"docs/evidence/imd-sourcify.json\":269,\"docs/evidence/imd-token-blockscout.json\":19,\"docs/evidence/independent-review-checks.json\":41,\"docs/evidence/mainnet-block.json\":20,\"docs/evidence/mainnet-fork-tests.txt\":11,\"docs/evidence/usdc-blockrazor.json\":49,\"docs/evidence/usdc-onchain.json\":71,\"docs/evidence/usdt-onchain.json\":59,\"docs/evidence/weth-onchain.json\":59,\"foundry.toml\":26,\"remappings.txt\":2,\"script/DeployMainnet.s.sol\":41,\"src/GovernanceTimelock.sol\":76,\"src/IMDBank.sol\":617,\"src/RiskOracle.sol\":126,\"src/lib/ExactToken.sol\":58,\"test-fork/Mainnet.t.sol\":144,\"test/Audit.t.sol\":189,\"test/DeploymentRehearsal.t.sol\":56,\"test/GovernanceTimelock.t.sol\":77,\"test/IMDBank.t.sol\":255,\"test/Invariants.t.sol\":145,\"test/RiskControls.t.sol\":101,\"test/RiskOracle.t.sol\":116,\"test/helpers/BankFixture.sol\":78,\"test/helpers/Mocks.sol\":126,\"tools/check_bytecode.py\":22,\"tools/economic_stress.py\":49,\"web/_headers\":11,\"web/abi.js\":33,\"web/app.js\":366,\"web/check-abi.mjs\":2,\"web/config.json\":19,\"web/core.js\":85,\"web/fonts.css\":1,\"web/index.html\":64,\"web/package.json\":9,\"web/styles.css\":2,\"web/tests/abi-compatibility.mjs\":20,\"web/tests/core.test.mjs\":90,\"web/tests/fork-integration.mjs\":162,\"web/vendor/ETHERS-LICENSE.md\":21,\"web/vendor/README.md\":10,\"web/vendor/ethers-6.15.0.min.js\":1},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1879,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/GovernanceTimelock.sol:48: GovernanceTimelock.cancel(bytes32) (src/GovernanceTimelock.sol#48-53) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/IMDBank.sol:558: IMDBank._previewIndex(IMDBank.Reserve) (src/IMDBank.sol#558-575) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/GovernanceTimelock.sol:55: Reentrancy in GovernanceTimelock.execute(address,bytes,bytes32) (src/GovernanceTimelock.sol#55-75):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:240: Reentrancy in IMDBank.donateLiquidity(address,uint256) (src/IMDBank.sol#240-247):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:222: Reentrancy in IMDBank.repay(address,uint256,address) (src/IMDBank.sol#222-238):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:203: Reentrancy in IMDBank.borrow(address,uint256,address) (src/IMDBank.sol#203-219):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:249: Reentrancy in IMDBank.coverBadDebt(address,uint256) (src/IMDBank.sol#249-258):\n[medium/medium] reentrancy-no-eth at src/IMDBank.sol:265: Reentrancy in IMDBank.liquidate(address,address,uint256,uint256,uint256) (src/IMDBank.sol#265-287):\n[medium/medium] uninitialized-local at src/IMDBank.sol:451: IMDBank._liquidationQuote(address,address,uint256).quote (src/IMDBank.sol#451) is a local variable never initialized\n[low/medium] missing-zero-check at src/GovernanceTimelock.sol:55: GovernanceTimelock.execute(address,bytes,bytes32).target (src/GovernanceTimelock.sol#55) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMDBank.sol:134: IMDBank.constructor(address,address,address,address,address,address,address).collateral_ (src/IMDBank.sol#134) lacks a zero-check on :\n[low/medium] reentrancy-events at src/GovernanceTimelock.sol:55: Reentrancy in GovernanceTimelock.execute(address,bytes,bytes32) (src/GovernanceTimelock.sol#55-75):\n[low/medium] timestamp at src/RiskOracle.sol:115: RiskOracle._read(address,uint8,uint32) (src/RiskOracle.sol#115-125) uses timestamp for comparisons\n[low/medium] timestamp at src/IMDBank.sol:265: IMDBank.liquidate(address,address,uint256,uint256,uint256) (src/IMDBank.sol#265-287) uses timestamp for comparisons\n[low/medium] timestamp at src/GovernanceTimelock.sol:55: GovernanceTimelock.execute(address,bytes,bytes32) (src/GovernanceTimelock.sol#55-75) uses timestamp for comparisons\n[low/medium] timestamp at src/IMDBank.sol:558: IMDBank._previewIndex(IMDBank.Reserve) (src/IMDBank.sol#558-575) uses timestamp for comparisons\n[low/medium] timestamp at src/GovernanceTimelock.sol:48: GovernanceTimelock.cancel(bytes32) (src/GovernanceTimelock.sol#48-53) uses timestamp for comparisons","passed":true},{"durationMs":562,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/GovernanceTimelock.sol:70: Reentrancy: State change after external call (3 places)\n[low] costly-loop at src/IMDBank.sol:501: Costly operations inside loop (2 places)\n[low] large-numeric-literal at src/IMDBank.sol:22: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/IMDBank.sol:152: Literal Instead of Constant (21 places)\n[low] local-variable-shadowing at src/IMDBank.sol:59: Local Variable Shadows State Variable\n[low] missing-inheritance at src/RiskOracle.sol:16: Missing Inheritance\n[low] require-revert-in-loop at src/IMDBank.sol:340: Loop Contains `require`/`revert` (4 places)\n[low] state-variable-could-be-immutable at src/IMDBank.sol:34: State Variable Could Be Immutable\n[low] unchecked-return at src/IMDBank.sol:273: Unchecked Return (3 places)\n[low] uninitialized-local-variable at src/IMDBank.sol:152: Uninitialized Local Variable (7 places)\n[low] unused-public-function at src/IMDBank.sol:311: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"aaeadfb60c0255006f3fa8e8c0e4cee9feb92d7aa0f9f0783c216e8232dc3858","verifiedTreeHash":"a32dd3288e33ba83c363fa7d41f13b88e910029f","verifierVersion":"0.1.0+e6140b7a"}]}