{"assessments":[],"deployments":[{"attestationHash":"cef7dee07882b0ef188f8f047274b8d73216aec4e8cc7338606f2f3d2b0cd339","chainId":11155111,"contracts":[{"address":"0x79d4f28dbcd48836f80fe3e851419e67c6ce639a","blockNumber":11920137,"name":"MockUSDC","role":"other","txHash":"0x9673bdbfbf0209093b6230202d02dde3958e901b7c20f36f5e4b8a3b05e97f0a"},{"address":"0xa981bb206daf2ceb22b879ed6c62519f37332756","blockNumber":11920137,"name":"CardOracle","role":"other","txHash":"0x9673bdbfbf0209093b6230202d02dde3958e901b7c20f36f5e4b8a3b05e97f0a"},{"address":"0x225e41fed68048d95a0c5f7d3a04f792c23cf585","blockNumber":11920137,"name":"CardPool","role":"other","txHash":"0x9673bdbfbf0209093b6230202d02dde3958e901b7c20f36f5e4b8a3b05e97f0a"},{"address":"0x4d1dd1082e3841dc9b6f900252a15b5ef11ec4ac","blockNumber":11920137,"name":"MockCardNFT","role":"other","txHash":"0x9673bdbfbf0209093b6230202d02dde3958e901b7c20f36f5e4b8a3b05e97f0a"},{"address":"0xecd0992da58c974f4fa882b2ec8bde44d3bce7db","blockNumber":11920137,"name":"CardLending","role":"other","txHash":"0x9673bdbfbf0209093b6230202d02dde3958e901b7c20f36f5e4b8a3b05e97f0a"}],"id":"c346908e-8af8-4f68-8c39-9e9acc217e46","manifestHash":"0f08311f37c238a6e1f5f41e880d6beb24acf239009510f4f2cc7054bfb75799","status":"live"}],"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":"3d045c98-285b-45fe-8302-42d18fc8bf05","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"47bbbe701ac3ceaacbcc287adb9c30afbaf54da16581c6417584aaaf6d79c957","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":"6afeef8ddd7f522725a19766bb0f2c2d4c6359e1aaddab61d9a7d618a2c0eb9c","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":"aa34f9a0eaf6281c4d9057f6ef7eb250a8630767d50c57774031a41923caa291","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"cc1870b96c31ee37fb2c4b3632cfbb04ae0a6d008689fb2adf25f58d6084956c","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":"7ff0bfbdcf1f30372d28f8a8f32df013a7b4d5019401741073eb429bd23d50ca","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":"37749211af0e5d9438455cd0dda2f127559a7b8db4ea2e4f220d3f2985d98451","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"86e6d086381200ef662ea72bcee0f5b61392b62733004e4b8d95e29858f99eda","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"1cb70841f7e6011627ab5477f961c25439db17fa2e06d2e19527e3ecd843f8cf","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build CardVault, a lending protocol where vaulted trading-card NFTs are collateral and prices come from the IdentityMD oracle. Deploy to Sepolia with the owner wallet as admin. Contracts: (1) MockUSDC (6 decimals, mintable). (2) MockCardNFT, ERC-721 standing in for a vaulted-card token such as Courtyard: metadata fields game, set, cardName, cardNumber, grader, grade, certNumber, plus a classId (bytes32) that groups identical card+grade. (3) CardPool, ERC-4626 over MockUSDC; lenders deposit and withdraw subject to utilisation. (4) CardOracle, the oracle consumer: holds an allowlist classId → question template parameters (PriceCharting product URL path, grade column); requestPrice(classId) pulls 0.5 test IMD from the caller, approves Intake, builds the oracle.request JSON body and calls Intake.requestWithFailure with this contract as callback target; onPrice(bytes32 requestId, OracleAttestation.Attestation a, bytes signature) accepts calls only from Intake, only for requests it opened and not yet settled, verifies the EIP-712 signature against this contract’s domain and the configured oracle signer, requires a.agreed >= a.quorum, decodes the numeric USD answer and stores {price, issuedAt, expiresAt} per classId; onFailure(bytes32, uint8, bytes32, uint16, uint16, bytes) clears the pending flag and moves no value; also a permissionless submit(attestation, signature) for relayed delivery. (5) CardLending: borrow(tokenId, amount) requires a fresh attestation (issuedAt within 24h) for the NFT’s classId, amount <= 45% of price, pulls the NFT, draws USDC from CardPool, 30-day term, interest accrues linearly at a rate set by the owner (initial 12% APR) plus a 1% origination fee to the pool; repay, extend (new attestation, same LTV check), liquidate after maturity or if a newer attestation puts LTV above 60%: 24-hour Dutch auction from 120% of debt down to 50%, proceeds repay the pool, surplus to the borrower, shortfall absorbed by a reserve funded from the origination fee. Pausable by a guardian; two-step admin hand-over; no upgradeability. Sepolia addresses: Intake 0xa43e6f75ee006411f79ac1c84120606c2330de82, test IMD 0x44a1cd38474fb1748400e7deb5f8d786cce3f89a (mintable), action id bytes32(“oracle.request@oracle-1”). Oracle body template: question “On pricecharting.com, on the product page for “{cardName} #{cardNumber}” in “{setName}” (URL starting https://www.pricecharting.com/game/{setSlug}/{cardSlug}), what is the price shown in the “{gradeColumn}” column at the time of reading within the pinned window?”, definitions term = the single number displayed in that column, period = value when read within the block window, unit = US dollars, rounding = whole dollars rounded down; answer type number; public sources; tolerance 500 bps; validForSeconds 86400; consumer = {chainId 11155111, verifyingContract = CardOracle}. Read the oracle-consumer and oracle-assess skills for the exact Attestation struct and domain, and verify the body with POST /requests/check before relying on it. Tests: unit tests for every path, a mocked-signer test for onPrice and submit, replay and wrong-request rejection, auction math at the boundaries, 4626 rounding, and a fork or scripted test on Sepolia that opens one real oracle request for Charizard #4 in Pokemon Base Set, PSA 10, and borrows against a minted mock card once the callback lands. Deliver source, tests, a README with the Sepolia addresses and a runbook for adding a card class.\n\nToken name: C\nToken symbol: AUREON\nTotal supply: 1000000","parentJobId":null,"planHash":"cfe2bac40bb9a5168c5d24a18e1d9a2db39ea3ffeda73630c67e8a4f0ac7e3d4","previousHash":"2550497f3d5a1215376250b0985f6460fa4451bc4a6404b679f1de7ce9edc3bb","projectId":"3d045c98-285b-45fe-8302-42d18fc8bf05","publication":{"commit":"ef5a93b696798b861b99e5e0ce31651431eb5cd2","deliveredAt":"2026-10-15T11:28:21.447Z","repoUrl":"https://github.com/identity-md-launches/launch-1828-c"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":null,"snapshotHash":"7634dec4edda54f25b406a865d644ab6201ea74a089bea9f8748556ac48e280d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"5068d115747c0802ad6b54db460ef1f507b23ccec3d21fbd4f44cade7962c584","device":"2b4e97bb63195f54","findings":[],"hash":"062d5a4b98e62f3a81dabf7d472e73f8a32d2b74144edf756ace7154017417ec","nodeId":"b922fdfe-00c2-40f4-902d-763cc947a9e5","outcome":"completed","summary":"Added adversarial tests, fuzz cases, and stateful invariants under `test/`.\n\nValidation: `forge build` succeeded; seeded `forge test` reports **125 passed, 0 failed, 2 skipped**. Invariants exercised 40,960 randomized calls.\n\nNo implementation defects reproduced. Live Sepolia checks remain unrun; setup is documented in [test/README.md](test/README.md).","treeHash":"83ab6d982b174b2dee1a28381051d8390530e1a7","usage":{"cachedInputTokens":4131968,"inputTokens":181607,"model":"gpt-6-astra","outputTokens":46373,"runtime":"codex","turns":10,"wallClockMs":2068716}},{"artifacts":[],"attempt":1,"bundleHash":"2c3d3d6902e004a727c7d85c48505dd382ef38b7f70db3cd978a68e5e1311558","device":"6b0a28df3d585600","findings":[],"hash":"0bc6e3cf400874a363027f2ef445de208effc1094e906a85b974ad6fa54736fa","nodeId":"ff7cc521-86ed-4e78-8196-f1a867b65180","outcome":"completed","summary":"The responses file is excluded by the repository's own git exclude rule, which the harness manages, so it is in place as required and intentionally not part of the diff.\n\n## Summary\n\nBoth reopened findings reproduced on the starting tree and are fixed; the reviewer's proof tests now pass alongside the existing suite. All 83 tests pass with the pinned fuzz seed, `forge build` and `forge fmt --check` are clean, and every finding is answered in `.imd-responses.json`.\n\n**Reopened findings (fixed)**\n\n- **Pool NAV at face value during auctions.** The lending contract now keeps an auction book and marks each open auction at the lesser of its frozen debt and its current Dutch price: par for the first 24,685 seconds, down the curve to 50% at 24 hours, and zero once unsold for a further day. The pool's `totalAssets` adds those marks plus the slice of the reserve that settlement will spend. Auctions open in time order, so the marks are kept as aggregates walked lazily from three queue heads, with no per-auction iteration in deposit or withdraw. A lender exiting mid-auction now carries their share of the expected loss, and a late floor sale shows up as a gain.\n- **Zero-dollar answer stored as a price.** A decoded answer of zero is refused with `BadAttestation`, and the body's guard now sends a minimum of 1 so the door refuses it before signing. The two tests that pinned the old behaviour were replaced with tests that the healthy loan stays active and cannot be bought.\n\n**Advisory findings**\n\n- Fixed: clock-skew rejection in `_settle` (the library's five-minute tolerance now applies), extend's LTV check (principal only, so a loan drawn at the cap renews at an unchanged price), a `maxAprBps` bound on `extend`, dust-safe `maxRedeem`, interest rounded up per loan and in NAV, and the guardian seat following ownership when the outgoing owner holds it.\n- Disputed: the strategic-default economics are the auction the brief specifies. No code changed; the README now spells out that trust assumption.\n\n**Left as designed, documented in the README:** post-maturity interest on a loan nobody has liquidated stays at contractual value, since liquidation at maturity is permissionless and needs no oracle. `withdraw(0)` and `redeem(0)` still revert.\n\nThe deploy inputs are unchanged. The only interface change for integrators is the new second argument on `extend`.","treeHash":"52b2a7ae16f50e2e25e822949d90fb287fe2a15d","usage":{"cachedInputTokens":2605110,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":62059,"runtime":"claude","turns":61,"wallClockMs":872650}},{"artifacts":[],"attempt":1,"bundleHash":"e7d844d3b9a327902f9bbf33d4b1ccdf9ce0bb682c7b5c705bd61a2552f33c92","device":"b97527597dd934a0","findings":[{"description":"The revised settle() makes every settlement NAV-neutral by holding back, as reserve, whatever a payment collects above the loan's current mark, and the checkpoint in _covered() lets reserve added after a mark-down cover only later mark-downs. This closes the previous round's sandwich (confirmed by unit, fuzz and invariant tests in this suite). The consequence is that a recovery on a loan whose loss lenders already carried never reaches them: after a 400 USDC loan is written off and sold at its 200 USDC floor, the 200 USDC sits in `reserve`, is excluded from availableLiquidity(), maxWithdraw() and totalAssets(), and there is no owner or lender function that moves it. It is spent only if another auction later marks down, benefiting whoever is a lender at that time. With no further auctions it stays in the contract indefinitely. The root README discloses this trade-off; it is recorded here so the audit judge sees it beside the fix it motivated, not because a test can show a wrong result.","line":152,"path":"src/CardPool.sol","reproduction":"Fixture: lender deposits 10,000 USDC; alice borrows 400 USDC (APR 0) against a 1,000 USDC card; warp to maturity; liquidate; warp 2 days (mark 0, NAV 9,604); bob buys at the 200 floor. Expected by a lender: some path to the 200 recovered on a loan they funded. Actual: pool.reserve() == 200e6, pool.totalAssets() == 9,604e6, pool.maxWithdraw(lender) ~ 9,604e6, and after the lender redeems everything the pool still holds 200e6 with totalSupply ~ 0; a later depositor's shares are priced on the empty NAV so the 200 accrues to nobody. See test/CardSettlementEdges.t.sol::test_heldBackReserveIsNeverLenderCashAndHasNoRelease.","severity":"info","title":"Held-back recoveries and post-loss reserve have no release path to the lenders who carried the loss"}],"hash":"1cb70841f7e6011627ab5477f961c25439db17fa2e06d2e19527e3ecd843f8cf","nodeId":"b922fdfe-00c2-40f4-902d-763cc947a9e5","outcome":"completed","summary":"The revision is complete. The full suite passes with the verifier's fuzz seed, and the invariant campaigns also pass under a second seed.\n\n**What changed in the implementation since my last round.** The builder answered my previous sandwich finding. The pool's `settle` now takes four arguments and keeps NAV exactly unchanged across every repayment, extension and purchase. Recoveries above a loan's mark are held back as reserve, the reserve counted in NAV is checkpointed so later fees or donations cover only later mark-downs, and an unsold auction is written down linearly from half its debt to zero over the second day instead of in one step. The book accessors were renamed from floor to decay.\n\n**What I did to the tests.**\n- Fixed the three compile breaks against the new interface and retargeted four unit tests whose expected numbers encoded the old step-at-write-off accounting. Each now asserts the NAV-neutral outcome and the resulting reserve, with the arithmetic spelled out in comments.\n- Rewrote the vault invariant handler's model. Every settlement is wrapped in a snapshot that checks NAV is unchanged, the pool receives exactly the payment, the reserve spends at most the counted cover, hold-back never exceeds the recovery above the released mark, and no lender's claim moves. Draws and reserve funding must move no NAV, liquidation at most one rounding unit, and time alone may only lift NAV by accrued interest or lower it by the mark-down. A new `sandwich` action deposits, settles any loan in any phase, and redeems in one block, asserting the actor recovers none of the cost. The witness test exercises it at par, mid-descent, after write-off and on an active repayment.\n- Added `test/CardSettlementEdges.t.sol` with thirteen tests: purchases at par, mid-descent and during the write-down, the branch where surplus counted cover is released rather than held back, fees and one-unit donations after a loss covering only later mark-downs, the rounding unit an extension holds back, the recovery no lender can withdraw, and two 1,000-run fuzz tests for NAV-neutral settlement with sandwiches in every phase and for NAV never rising with time alone.\n- Updated `test/README.md` to describe the new model and limits.\n\n**Findings.** No new defect with a failing input was found in the revised code, and the earlier finding is confirmed fixed. I recorded one informational note in `.imd-findings.json`: recoveries held back after a write-down have no release path, so lenders who carried the loss never receive a later floor sale of that loan unless another auction marks down while they are still invested. The root README discloses this trade-off, and the suite asserts it as implemented rather than as desirable.\n\nFinal result:\n\n| Run | Outcome |\n| --- | --- |\n| `forge test` with the verifier's seed | 164 passed, 0 failed, 2 skipped (fork tests without an RPC) |\n| Invariant campaigns with a second seed | 4 passed |\n\nThe two Sepolia fork tests still skip without an RPC URL, and the live paid request, callback and borrow sequence remains owed to a run with network access.","treeHash":"4309bfd6f57c0575b125d3fad9f1f1ebda68ef24","usage":{"cachedInputTokens":4120888,"inputTokens":994,"model":"claude-fable-5-1","outputTokens":65892,"runtime":"claude","turns":73,"wallClockMs":1129963}},{"artifacts":[],"attempt":1,"bundleHash":"21dd488817d586fce06c175159cdbe57151e662d9be00102439c0c52832ee2b0","device":"3f02755d8661ddf1","findings":[],"hash":"316da746d21a566836c62e1b685bb7e17588090af4afa5c4ed0d0a6d35ab7a02","nodeId":"ff7cc521-86ed-4e78-8196-f1a867b65180","outcome":"completed","summary":"Implemented all five CardVault contracts, tests, vendored dependencies, and the [Sepolia runbook](README.md).\n\nVerified:\n- `forge build`\n- `forge test`: 70 passed\n- `forge fmt --check`\n- Python tests: 4 passed\n\nDeployment and the real oracle-callback borrowing test remain launch-operator steps; the supplied smoke script covers that flow.","treeHash":"571af6bc22a50694d62c960fd8557a5ff4fd556d","usage":{"cachedInputTokens":3516544,"inputTokens":143514,"model":"gpt-6-astra","outputTokens":61477,"runtime":"codex","turns":9,"wallClockMs":2389114}},{"artifacts":[],"attempt":1,"bundleHash":"ff5f7be29f4411876b5fbb64373d2bb2a98324a59ea4b3812b136edfb70c1ba8","device":"3baa2091028c32c1","findings":[],"hash":"37749211af0e5d9438455cd0dda2f127559a7b8db4ea2e4f220d3f2985d98451","nodeId":"ff7cc521-86ed-4e78-8196-f1a867b65180","outcome":"completed","summary":"The work is done to the extent this machine allowed. Summary:\n\n**Finding 965d7546 (settlement NAV jump captured by a same-block deposit/redeem): reproduced by tracing the accounting, and fixed at the root.**\n\nWhat I changed:\n\n- **`CardPool.settle` is now NAV-neutral by construction.** It takes four arguments: payment, debt, and the exact drop in receivable and in expected shortfall that `CardLending` measured around the book change. The pool spends only the reserve that NAV had counted as covering that loan's shortfall, and holds back as reserve everything the payment collects above the loan's mark, less any counted reserve the settlement leaves with nothing to back. `totalAssets()` is identical before and after every repay, extend and buy, so the sandwich in both of the reviewer's cases nets zero: bob's floor purchase costs exactly the 200 USDC floor, alice's repay costs exactly the 400 USDC debt, and the lender's claim never drops.\n- **Counted reserve is checkpointed, not `min(reserve, shortfall)`.** `coveredReserve()` grows one for one with expected shortfall since the last checkpoint, capped by reserve and shortfall. `draw`, `fundReserve` and `settle` checkpoint first, so held-back recoveries, fees and donations never re-rate an already recognised loss. They reach lenders only by covering later mark-downs, which is the behaviour the reviewer asked for.\n- **The write-off cliff is gone.** The mark now decays linearly from the 50% floor to zero over `WRITE_OFF_DELAY`, tracked by `decayDebt`/`decayWeight` aggregates in place of `floorMark`. NAV is continuous in time, so no lender can time an exit around a step. The buy price still stays at the floor.\n- **Tests.** Six new tests in `test/CardLending.t.sol`: the reviewer's case A and case B sandwiches, repay during descent, held-back recovery covering only a later auction, reserve donation after a loss, and hourly NAV continuity over 50 hours. Existing curve and book tests were updated for the linear decay and the renamed aggregates. `test/CardPool.t.sol` updated for the new `settle` signature.\n- **Docs.** README now describes the settlement rule, the counted reserve, the linear write-off and the trade-off that recoveries stay in reserve. `.imd-responses.json` answers the finding with verdict `fixed`.\n\n**What I could not verify.** The Bash sandbox on this worker refused every command, including `echo`, with a nested user-namespace seccomp error, and the no-sandbox override is disabled in this session. I therefore could not run `forge build`, `forge test` or `forge fmt --check`. I checked the arithmetic of every new and changed test by hand against the contract code, and re-read both modified contracts for syntax, but a compile or test run on a working machine is still required before trusting this revision. That limitation is also stated in the responses file.","treeHash":"42d53dbb69b71891bde6a116c70f70fb8838d8cc","usage":{"cachedInputTokens":3437334,"inputTokens":834,"model":"claude-fable-5-1","outputTokens":96761,"runtime":"claude","turns":62,"wallClockMs":1288529}},{"artifacts":[],"attempt":1,"bundleHash":"65c97563a5c50aaab6bd9f1f122246f2cdda5b9445e7a00c82634a35c2408625","device":"75052237a39b6e12","findings":[{"description":"CardLending.auctionMark/_projectBook carry an auction at zero once it has been unsold for WRITE_OFF_DELAY past the floor, and CardPool.totalAssets uses that mark. The sale price at the floor is unchanged (50% of debt), so when a written-off card is finally bought, pool NAV jumps by the whole sale proceeds in one transaction. Nothing stops the buyer from depositing into the pool immediately before buy() and redeeming immediately after, so the buyer takes a pro-rata share of the recovery that the pool's existing lenders were carrying the loss for. With a deposit equal to the pool the buyer takes about half of the proceeds; with a deposit much larger than the pool the buyer takes nearly all of them, which makes the floor purchase close to free. The brief's loss waterfall is proceeds to the pool, shortfall from the reserve; this marking lets a third party intercept the proceeds before lenders see them. Possible fixes that keep the agreed design: keep the floor mark (ceil(debt/2)) instead of writing off to zero, or credit sale proceeds of a written-off loan to the reserve (which only counts toward NAV up to expected shortfall) rather than to unreserved cash, or add a deposit-to-redeem delay. The reporter's own suite contains no test that asserts the current behaviour as correct.","line":197,"path":"src/CardLending.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\nimport {MockCardNFT} from \"src/MockCardNFT.sol\";\nimport {CardPool} from \"src/CardPool.sol\";\nimport {CardOracle} from \"src/CardOracle.sol\";\nimport {CardLending} from \"src/CardLending.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract ProofIntake is IIntake {\n    uint256 internal nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata, bytes4, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32)\n    {\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        return keccak256(abi.encode(\"proof request\", ++nonce));\n    }\n\n    function deliver(address target, bytes32 id, OracleAttestation.Attestation calldata a, bytes calldata sig) external {\n        (bool ok,) = target.call{gas: 200_000}(abi.encodeWithSelector(CardOracle.onPrice.selector, id, a, sig));\n        require(ok, \"callback failed\");\n    }\n}\n\n/// @dev A written-off auction is carried at zero in pool NAV. Its floor sale then lifts NAV in one step,\n/// so the buyer can deposit just before buying and redeem just after, taking most of the recovery that\n/// the pool's existing lenders were owed. Expected: the lender's claim after the sale does not depend on\n/// whether the buyer also deposited. Actual: the lender loses about half of the recovery here.\ncontract WriteOffRecoveryCaptureTest is Test {\n    uint256 internal constant SIGNER_KEY = 0xA11CE;\n    address internal alice = makeAddr(\"alice\");\n    address internal bob = makeAddr(\"bob\");\n    address internal lender = makeAddr(\"lender\");\n    MockUSDC internal usdc;\n    MockCardNFT internal nft;\n    CardPool internal pool;\n    CardOracle internal oracle;\n    CardLending internal lending;\n    ProofIMD internal imd;\n    ProofIntake internal intake;\n    bytes32 internal cardClass;\n    uint256 internal tokenId;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        usdc = new MockUSDC(address(this));\n        nft = new MockCardNFT(address(this));\n        pool = new CardPool(address(this), usdc);\n        imd = new ProofIMD();\n        intake = new ProofIntake();\n        oracle = new CardOracle(address(this), address(intake), address(imd), vm.addr(SIGNER_KEY));\n        lending = new CardLending(address(this), nft, pool, oracle, usdc);\n        pool.setLending(address(lending));\n        lending.setAPR(0);\n        MockCardNFT.Card memory card = MockCardNFT.Card(\"Pokemon\", \"Pokemon Base Set\", \"Charizard\", \"4\", \"PSA\", \"10\", \"1\");\n        cardClass = nft.computeClassId(card);\n        tokenId = nft.mint(alice, card);\n        oracle.setClass(\n            cardClass,\n            CardOracle.Template(\"Charizard\", \"4\", \"Pokemon Base Set\", \"pokemon-base-set\", \"charizard-4\", \"PSA 10\"),\n            true\n        );\n        imd.mint(alice, 10 ether);\n        usdc.mint(bob, 100_000e6);\n        usdc.mint(lender, 10_000e6);\n        vm.startPrank(alice);\n        imd.approve(address(oracle), type(uint256).max);\n        nft.setApprovalForAll(address(lending), true);\n        vm.stopPrank();\n        vm.startPrank(bob);\n        usdc.approve(address(lending), type(uint256).max);\n        usdc.approve(address(pool), type(uint256).max);\n        vm.stopPrank();\n        vm.startPrank(lender);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lender);\n        vm.stopPrank();\n    }\n\n    function test_jitDepositorCapturesWrittenOffRecoveryFromLenders() public {\n        _publish(1000);\n        vm.prank(alice);\n        uint256 id = lending.borrow(tokenId, 400e6);\n        vm.warp(lending.loan(id).maturity);\n        lending.liquidate(id);\n        vm.warp(vm.getBlockTimestamp() + 2 days); // unsold through the floor: written off\n        assertEq(lending.auctionMark(id), 0);\n        uint256 lenderClaimBefore = pool.convertToAssets(pool.balanceOf(lender));\n        assertApproxEqAbs(lenderClaimBefore, 9_604e6, 1, \"lender carried at cash plus 4e6 reserve cover\");\n\n        // Reference outcome: the floor sale alone, no deposit, leaves the lender with the whole recovery.\n        uint256 snapshot = vm.snapshotState();\n        vm.prank(bob);\n        lending.buy(id, 200e6, bob);\n        uint256 lenderClaimPlainSale = pool.convertToAssets(pool.balanceOf(lender));\n        assertApproxEqAbs(lenderClaimPlainSale, 9_804e6, 1);\n        vm.revertToState(snapshot);\n\n        // Same sale wrapped in a deposit and a redemption by the buyer, all in one block.\n        vm.startPrank(bob);\n        uint256 shares = pool.deposit(10_000e6, bob);\n        lending.buy(id, 200e6, bob);\n        uint256 returned = pool.redeem(shares, bob, bob);\n        vm.stopPrank();\n        uint256 lenderClaimSandwiched = pool.convertToAssets(pool.balanceOf(lender));\n\n        emit log_named_uint(\"lender claim, plain floor sale\", lenderClaimPlainSale);\n        emit log_named_uint(\"lender claim, sale wrapped in deposit/redeem\", lenderClaimSandwiched);\n        emit log_named_uint(\"buyer deposit returned\", returned);\n        assertEq(nft.ownerOf(tokenId), bob);\n        assertGe(returned, 10_000e6, \"buyer never loses on the round trip\");\n        assertEq(\n            lenderClaimSandwiched, lenderClaimPlainSale, \"a buyer's same-block deposit must not take the lender's recovery\"\n        );\n    }\n\n    function _publish(uint256 dollars) internal {\n        vm.prank(alice);\n        bytes32 id = oracle.requestPrice(cardClass);\n        OracleAttestation.Attestation memory a;\n        a.requestId = keccak256(abi.encode(\"uuid\", dollars, vm.getBlockTimestamp()));\n        a.chainId = 1;\n        a.questionHash = keccak256(\"question\");\n        a.answerType = 3;\n        a.answer = abi.encode(dollars);\n        a.figure = dollars;\n        a.fromBlock = 100;\n        a.toBlock = 900;\n        a.blockHash = keccak256(\"block\");\n        a.panelJobId = keccak256(\"panel\");\n        a.panelSize = 30;\n        a.quorum = 21;\n        a.agreed = 25;\n        a.issuedAt = uint64(vm.getBlockTimestamp());\n        a.expiresAt = a.issuedAt + 1 days;\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, oracle.attestationDigest(a));\n        intake.deliver(address(oracle), id, a, abi.encodePacked(r, s, v));\n    }\n}","reproduction":"Lender deposits 10,000 USDC. Alice borrows 400 USDC against a $1000 card at 0% APR, is liquidated at maturity, and nobody buys for 2 days so the auction is written off (auctionMark == 0, NAV == 9,604 USDC including the 4 USDC reserve cover). Plain floor sale by Bob at 200 USDC: lender's claim becomes 9,804 USDC (expected). Same sale with Bob calling pool.deposit(10,000) before buy() and pool.redeem(all) after, in one block: lender's claim is 9,701.98 USDC and Bob's 10,000 deposit returns 10,102.02 USDC (actual). Bob effectively paid 98 USDC for a card listed at a 200 USDC floor; lenders lost 102 USDC of recovery. forge test --match-path test/scratch/WriteOffRecoveryCapture.t.sol fails on the current code with 'a buyer's same-block deposit must not take the lender's recovery: 9701980005 != 9804000000'.","severity":"medium","title":"Written-off auction recovery is capturable by a same-block deposit around the floor sale"}],"hash":"3bf6144010c95f589f51762fba3db3527eb37c5d39469a94ce306a8af4f80e92","nodeId":"b922fdfe-00c2-40f4-902d-763cc947a9e5","outcome":"completed","summary":"The findings file is excluded by the harness's own git exclude list, so it is picked up out of band as intended. Work is complete.\n\n## Summary\n\nThe implementation was revised after my previous round, and my accepted suite no longer compiled against it. I retargeted the suite, extended it to the new behaviour, and reported one defect.\n\n**What broke and how I repaired it**\n\n- Build failure: `extend` now takes a borrower APR cap. Existing calls pass the maximum cap so their meaning is unchanged.\n- Six runtime failures from the revision's semantic changes, all fixed in the tests rather than worked around: zero-dollar answers are now refused, so the oracle handler draws prices from one dollar upward and the adversarial fuzz case was swapped for the zero-figure rejection. Interest now rounds up, so the handler's aggregate-versus-per-loan bound flipped direction. Pool NAV now marks auctions down the Dutch curve with a write-off at two days, so the handler's NAV, draw, and reserve-funding checks use an independent per-loan mark model.\n\n**New coverage**\n\n- The vault handler now recomputes every auction's mark independently, bounds the book's one-time rounding, checks that marks plus expected shortfall partition the frozen debt, checks that an explicit sync changes nothing, and tries every renewal with a cap one point too low first.\n- The oracle handler has a new action delivering signed zero and oversize figures through both the callback and the relay, checking the request stays open and the UUID stays unconsumed.\n- `test/CardRevisionEdges.t.sol` adds ten unit tests for the guardian seat under pending nominations, the APR cap boundaries, LTV on principal after interest, one-unit interest rounding, the auction mark at every phase boundary, repayment and sale after write-off, out-of-order closure of a staggered auction pair, and future `issuedAt` within tolerance.\n\n**Result**\n\n| Check | Outcome |\n| --- | --- |\n| `forge build` | passes |\n| `forge test` with the verifier seed | 145 passed, 0 failed, 2 skipped (fork tests without an RPC) |\n| Paths changed | `test/**` only |\n\n**Finding reported, not asserted**\n\nA medium finding is in `.imd-findings.json` with a self-contained failing proof. Once an auction is written off, its floor sale lifts NAV in one step, so the buyer can deposit before `buy` and redeem after it and take a pro-rata share of the recovery from existing lenders. In the reproduction the buyer's 10,000 USDC deposit returns 10,102 USDC and the lender's claim drops by the same 102 USDC, so the card effectively costs 98 USDC against a 200 USDC floor. The delivered suite describes the marking as implemented but contains no test blessing this capture.\n\nThe optional Sepolia fork tests and a live paid request with a real callback remain unexecuted here, as before, since this environment has no network.","treeHash":"95f9ace6c0d261101a90476ee47217269ed4a10f","usage":{"cachedInputTokens":2782364,"inputTokens":770,"model":"claude-fable-5-1","outputTokens":51234,"runtime":"claude","turns":55,"wallClockMs":896296}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"11e4593175677f7e","findings":[{"citation":"resolved","description":"Settles the independent tester's finding 0fe9f2f7 (reproduced) and widens it to its root cause. The round-one fix marks each auction loan in NAV at min(frozen debt, current Dutch price) and at zero once unsold for WRITE_OFF_DELAY (src/CardLending.sol:197 `if (elapsed >= AUCTION_DURATION + WRITE_OFF_DELAY) return 0;`). Settlement, however, still moves the full proceeds into unreserved cash in one transaction: buy() of a written-off loan pays the unchanged floor (ceil(debt/2)) against a mark of 0, and repay() of any loan in State.Auction pays the full auctionDebt against a mark that is below the debt from PAR_WINDOW onward and zero after write-off. totalAssets() (the quoted line) therefore jumps by proceeds minus mark minus the reserve it had counted, within the settling transaction. CardPool has no deposit-to-redeem delay, deposit() and redeem() are plain nonReentrant calls, and buy()/repay() are permissionless, so the settler can deposit into the pool immediately before settling and redeem immediately after, taking proceeds * (own deposit / (own deposit + prior NAV)) of the gain. With capital equal to the pool the settler takes about half of the recovery; with capital much larger than the pool, nearly all of it. For the repay path this means a liquidated borrower with capital gets the NFT back for a few USDC net instead of the debt, and the pool, which had already recognised the loss against its lenders, never sees the recovery. The same step also runs the other way: the mark drops from ceil(debt/2) to 0 at the known timestamp auctionStartedAt + 2 days, so a lender who redeems in the block before it exits at the floor value and the remaining lenders carry the write-off. Brief's waterfall is proceeds to the pool, shortfall from the reserve; here a third party intercepts the proceeds. Fix that keeps the design: in CardLending/CardPool, treat any settlement payment above the loan's current NAV mark (plus the reserve counted for it) as reserve rather than unreserved cash, so NAV does not move at settlement and the recovery reaches lenders only through later shortfall coverage (or an owner release that is itself not sandwichable). A deposit-to-redeem holding period is not a fix: the settler chooses when to settle, so a deposit made before the sale still owns a share of the gain after any delay. Keeping the floor mark instead of writing off to zero removes only the buy() case (the repay() jump of debt - ceil(debt/2) remains), so it is not sufficient on its own.","line":56,"path":"src/CardPool.sol","reproduction":"State: lender deposits 10,000e6; owner setAPR(0); price 1000 delivered; alice borrows 400e6 against tokenId (fee 4e6 to reserve; pool cash 9,604e6, reserve 4e6). Warp +30 days, anyone calls liquidate(loanId) (maturity). Warp +2 days: auctionMark(loanId) == 0, auctionPrice(loanId) == 200e6, pool.totalAssets() == 9,604e6. Case A (buy): bob holds 10,200e6. In one block bob calls pool.deposit(10,000e6, bob), lending.buy(loanId, 200e6, bob), pool.redeem(all, bob, bob). Expected: bob's USDC plus any share value falls by exactly the 200e6 floor and the lender's claim does not fall below 9,604e6. Actual: bob's net cost is 97,980,005 (he holds 10,102,019,995 USDC) and the lender's claim is 9,701,980,005 instead of the 9,804e6 the sale should have left them: bob took about 102 USDC of the pool's recovery. Case B (repay): alice holds 1,000,400e6. In one block alice calls pool.deposit(1,000,000e6, alice), lending.repay(loanId) (pulls 400e6), pool.redeem(all, alice, alice). Expected: alice's net cost is 400e6 and the lender's claim rises to 9,996e6 (cash 10,000e6 less the 4e6 reserve). Actual: alice's net cost is 7,767,006 and the lender's claim is 9,607,767,006, so she recovers the $1000 card for 7.77 USDC. Before write-off the repay jump is still debt - ceil(debt/2) - reserve counted (192e6 here) because the mark at the floor is 200e6 while repay collects 400e6. Proof: forge test --match-path test/scratch/Proof_SettlementGainCapture.t.sol fails both tests on the current code: 'bob took part of the pool's recovery for himself: 97980005 < 199999998' and 'alice recovered her card for less than the debt: 7767006 < 399999998'.","severity":"medium","snippet":"        return availableLiquidity() + receivable + Math.min(reserve, expectedShortfall);","title":"Settling an auction loan for more than its NAV mark is an atomic pool gain that a same-block deposit/redeem captures, so a written-off floor sale or a full repay of an auctioned loan hands most of the"}],"hash":"43c2d15d21918d24c5fd1834c8a6cca44edb328e9c85a2be22b2b855ad336c40","nodeId":"e601e665-9ae0-4d2f-a02a-6d45305dce7e","outcome":"completed","summary":"The review is complete. The findings file is written, the scratch directory is removed, and the working tree holds no new tracked files.\n\n**Round-one findings settled**\n\n- **NAV at face value (79849ca7): fixed.** My proof now passes. Auction loans are marked at the lesser of debt and the Dutch price, then zero after a day at the floor, and the pool counts the reserve only up to the expected shortfall. One residual remains by design: a lender who exits inside the 24,685-second par window still leaves at par, which is the mark my finding asked for. The audit_flow proof that exits one hour after liquidation still fails for that reason, and the audit_math proof now fails only because it delivers a zero price, which the code refuses.\n- **Zero-dollar answer (3d0d0b4d): fixed.** Both proofs pass. A zero answer reverts in settlement and the body guard sends a minimum of 1.\n- **issuedAt skew, extend at the cap, maxRedeem dust, extend rate bound, interest rounding, guardian handover: all fixed** on the quoted lines. The strategic-default note stays a documented trust assumption.\n\n**One new finding, medium, with a proof**\n\nThe independent tester's write-off capture reproduces, and the root cause is wider than the write-off. Any settlement that collects more than the auction loan's current mark lands in unreserved cash in one transaction, and nothing separates a deposit from a redeem. The settler deposits first, settles, and redeems. Bob buys a written-off card at the 200 USDC floor for a net cost of about 98 USDC. Alice, the liquidated borrower, repays her 400 USDC debt in full with a large deposit around it and gets the card back for 7.77 USDC net. The lenders, who already absorbed the markdown, lose the recovery. The finding is anchored at the pool's NAV line and the proof contains both cases. The suggested fix routes proceeds above the mark into the reserve. A holding period is not a fix because the settler chooses when to settle, and keeping the floor mark removes only the buy case.\n\n**Coverage**\n\nAll 60 entry points are answered. The repository suite passes with 145 tests and 2 skipped fork tests. The manifest lists five contracts in dependency order with correct argument counts, the reference's Sepolia addresses and signer, and every runtime size is under the limit.","treeHash":null,"usage":{"cachedInputTokens":1302344,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":31869,"runtime":"claude","turns":24,"wallClockMs":454673}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9df7d5d52e83c572","findings":[{"citation":"resolved","description":"CardPool.totalAssets() (src/CardPool.sol:52-54) is cash minus reserve plus CardLending.totalDebt(), and totalDebt() adds frozenDebt, the full principal plus interest of every loan sitting in State.Auction. That receivable is carried at 100% from liquidate() until buy() or repay() settles it, and nothing ever writes it down: an auction that reaches its 50% floor with no buyer (the card is worth less than half the debt, or the class has no market) stays frozen in NAV indefinitely. During that window every share is priced as if the debt were good. A lender who redeems before the shortfall is realised receives their full pre-default share and the whole loss lands on whoever is still in the pool; a depositor who enters during the window pays full price for shares backed by a dead receivable. A watching lender can also front-run the floor-price buy() transaction (which is the moment settle() finally recognises the loss) with a withdraw() and avoid the loss entirely. Because liquidate() has no keeper reward and interest keeps accruing on an Active loan past maturity, the phantom part of NAV also grows at the loan APR until someone liquidates. Invariant broken: sum of previewRedeem over all holders must not exceed what the pool can ever recover; after the sequence below it exceeds it by the unsold debt minus the floor and reserve. Minimal fix that keeps the agreed design: value an auction loan in totalDebt() at min(frozen debt, current auctionPrice) (so NAV declines with the Dutch curve and stops at the floor), and add an owner or permissionless write-off of an auction that has sat at its floor for a bounded period so dead debt leaves NAV. The attached proof passes once totalDebt() values an auction loan at min(frozen debt, auctionPrice()); a fix that relies solely on a new write-off call needs that call added to the proof before the final assertion.","line":148,"path":"src/CardLending.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\nimport {MockCardNFT} from \"src/MockCardNFT.sol\";\nimport {CardPool} from \"src/CardPool.sol\";\nimport {CardOracle} from \"src/CardOracle.sol\";\nimport {CardLending} from \"src/CardLending.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract ProofIntake is IIntake {\n    struct Ask {\n        Callback callback;\n    }\n\n    mapping(bytes32 => Ask) private _asks;\n    uint256 public nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata callback, bytes4, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 id)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        id = keccak256(abi.encode(address(this), ++nonce));\n        _asks[id] = Ask(callback);\n    }\n\n    function deliver(bytes32 id, OracleAttestation.Attestation calldata a, bytes calldata signature) external {\n        Callback memory cb = _asks[id].callback;\n        (bool ok, bytes memory data) = cb.target.call{gas: 200_000}(abi.encodeWithSelector(cb.selector, id, a, signature));\n        if (!ok) {\n            assembly (\"memory-safe\") {\n                revert(add(data, 32), mload(data))\n            }\n        }\n    }\n}\n\n/// @notice Pool NAV counts a defaulted, unsold auction loan at face value forever. The first lender to exit\n/// takes the full pre-default value and the loss is left entirely on whoever stays.\ncontract NavBadDebtProof is Test {\n    uint256 internal constant SIGNER_KEY = 0xA11CE;\n    address internal alice = makeAddr(\"alice\");\n    address internal lenderA = makeAddr(\"lenderA\");\n    address internal lenderB = makeAddr(\"lenderB\");\n    MockUSDC internal usdc;\n    MockCardNFT internal nft;\n    CardPool internal pool;\n    CardOracle internal oracle;\n    CardLending internal lending;\n    ProofIMD internal imd;\n    ProofIntake internal intake;\n    bytes32 internal cardClass;\n    uint256 internal tokenId;\n    uint256 internal oracleNonce;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        usdc = new MockUSDC(address(this));\n        nft = new MockCardNFT(address(this));\n        pool = new CardPool(address(this), usdc);\n        imd = new ProofIMD();\n        intake = new ProofIntake();\n        oracle = new CardOracle(address(this), address(intake), address(imd), vm.addr(SIGNER_KEY));\n        lending = new CardLending(address(this), nft, pool, oracle, usdc);\n        pool.setLending(address(lending));\n        MockCardNFT.Card memory card = MockCardNFT.Card(\"Pokemon\", \"Pokemon Base Set\", \"Charizard\", \"4\", \"PSA\", \"10\", \"1\");\n        cardClass = nft.computeClassId(card);\n        tokenId = nft.mint(alice, card);\n        oracle.setClass(\n            cardClass, CardOracle.Template(\"Charizard\", \"4\", \"Pokemon Base Set\", \"pokemon-base-set\", \"charizard-4\", \"PSA 10\"), true\n        );\n        imd.mint(alice, 100 ether);\n        usdc.mint(lenderA, 10_000e6);\n        usdc.mint(lenderB, 10_000e6);\n        vm.startPrank(alice);\n        imd.approve(address(oracle), type(uint256).max);\n        nft.setApprovalForAll(address(lending), true);\n        vm.stopPrank();\n        vm.startPrank(lenderA);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lenderA);\n        vm.stopPrank();\n        vm.startPrank(lenderB);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lenderB);\n        vm.stopPrank();\n        lending.setAPR(0);\n    }\n\n    function deliverPrice(uint256 dollars) internal {\n        vm.prank(alice);\n        bytes32 id = oracle.requestPrice(cardClass);\n        OracleAttestation.Attestation memory a;\n        a.requestId = keccak256(abi.encode(\"oracle UUID\", ++oracleNonce));\n        a.chainId = 1;\n        a.questionHash = keccak256(\"canonical question document\");\n        a.answerType = 3;\n        a.answer = abi.encode(dollars);\n        a.figure = dollars;\n        a.fromBlock = 100;\n        a.toBlock = 900;\n        a.blockHash = keccak256(\"closing block\");\n        a.panelJobId = keccak256(\"panel UUID\");\n        a.panelSize = 30;\n        a.quorum = 21;\n        a.agreed = 25;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 1 days);\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, oracle.attestationDigest(a));\n        intake.deliver(id, a, abi.encodePacked(r, s, v));\n    }\n\n    function test_firstLenderExitsAtFaceValueOfUnsoldAuctionDebt() public {\n        // Alice borrows 450 USDC (45% of a $1000 card). Pool: 19,550 cash + 4.5 reserve, 450 receivable.\n        deliverPrice(1000);\n        vm.prank(alice);\n        uint256 loanId = lending.borrow(tokenId, 450e6);\n        assertEq(pool.totalAssets(), 20_000e6);\n\n        // The card's price collapses to $100: a $100 card now secures a 450 USDC debt.\n        vm.warp(block.timestamp + 1);\n        deliverPrice(100);\n        lending.liquidate(loanId);\n\n        // The auction runs its full 24 hours and reaches its permanent floor, 225 USDC, above what the card is\n        // worth. Nobody buys. The most the pool can ever recover is the floor plus the 4.5 USDC reserve.\n        vm.warp(block.timestamp + 1 days);\n        assertEq(lending.auctionPrice(loanId), 225e6);\n        uint256 cash = pool.availableLiquidity();\n        uint256 maxRecoverable = cash + lending.auctionPrice(loanId) + pool.reserve();\n        assertEq(maxRecoverable, 19_550e6 + 225e6 + 4_500_000);\n\n        // On the current code NAV still reports the dead receivable at face value (20,000e6).\n        emit log_named_uint(\"totalAssets at the unsold floor\", pool.totalAssets());\n\n        // Lender A redeems everything and must not receive more than half of what can ever be recovered.\n        uint256 sharesA = pool.balanceOf(lenderA);\n        vm.prank(lenderA);\n        uint256 received = pool.redeem(sharesA, lenderA, lenderA);\n        emit log_named_uint(\"lender A received\", received);\n        emit log_named_uint(\"fair half of recoverable\", maxRecoverable / 2);\n        assertLe(received, maxRecoverable / 2, \"exiting lender paid from the other lender's cash\");\n    }\n}","reproduction":"Two lenders each deposit 10,000 USDC (pool cash 20,000, shares 10,000e12 each). APR set to 0 for round numbers. Alice borrows 450 USDC against a card priced $1000 (fee 4.5 to reserve; cash 19,550, totalDebt 450, totalAssets 20,000). A new attestation prices the class at $100; anyone calls liquidate(loanId): frozenDebt = 450e6, auction opens at 540e6. Warp 1 day: auctionPrice = 225e6 and no one buys (card worth 100). Max the pool can ever recover = 19,550 cash + 225 floor + 4.5 reserve = 19,779.5, so a fair pro-rata exit is 9,889.75 per lender. Actual: totalAssets() still returns 20,000e6; lender A redeems 10,000e12 shares and receives 10,000,000,000 (10,000.00 USDC) > 9,889,750,000. Lender B is left with 9,550 cash backing a 10,000 claim; if the card is eventually sold at the floor B realises 9,779.5, a 220.5 USDC loss that should have been split 110.25 each. Expected: an exit during or after a failed auction pays at most the pro-rata share of recoverable assets. See test_firstLenderExitsAtFaceValueOfUnsoldAuctionDebt in the proof, which fails on the current code with '10000000000 > 9889750000'.","severity":"medium","snippet":"    function totalDebt() external view returns (uint256) {\n        return activePrincipal + frozenDebt + (interestNumerator + rateWeight * (block.timestamp - lastAccrual))\n            / INTEREST_DENOMINATOR;\n    }","title":"Pool NAV counts defaulted auction debt at face value forever: first lender to exit is paid from the remaining lenders' cash and there is no write-off"},{"citation":"resolved","description":"_settle() bounds the decoded answer only from above (dollars > MAX_USD) and the body's own guard sends \"min\":\"0\" (src/CardOracle.sol:164), so an attestation whose answer is 0 passes every check, is stored as Price(0, issuedAt, expiresAt) and is returned by freshPrice() for 24 hours. The body's definitions tell the panel 'If unavailable, report inability, not zero', which is exactly the reading a zero encodes: a PriceCharting grade column that shows no value (N/A, dash, no graded sales for a less-traded class) read by a 21-of-30 quorum as 0, with toleranceBps 500 of 0 meaning all zeros agree. The contract turns that failure reading into a valuation. CardLending.liquidate() (src/CardLending.sol:194-197) then finds debt > 60% of 0 for every Active loan of the class, regardless of real collateral value, and opens a Dutch auction at 120% of debt, which is far below a healthy card's value (max LTV 45% means the card was worth at least 2.2x debt at origination). Any searcher buys at the opening price; the borrower loses the card and receives only the 20%-of-debt surplus. The repository tests (test_zeroNewPriceAllowsLiquidation, test_zeroIsStoredButCannotSupportBorrowing) pin this behaviour in, so it is currently intended, but the end state contradicts the protocol's own rule that liquidation before maturity requires a newer attestation 'that puts LTV above 60%': an unavailable price is not an LTV. Fix within the agreed design: treat dollars == 0 as no price (revert BadAttestation in _settle, or at minimum refuse it in freshPrice()) and send \"min\":\"1\" in the guard so the door refuses the answer before it is signed.","line":266,"path":"src/CardOracle.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\nimport {MockCardNFT} from \"src/MockCardNFT.sol\";\nimport {CardPool} from \"src/CardPool.sol\";\nimport {CardOracle} from \"src/CardOracle.sol\";\nimport {CardLending} from \"src/CardLending.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ProofIMD2 is ERC20 {\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract ProofIntake2 is IIntake {\n    mapping(bytes32 => Callback) private _callbacks;\n    uint256 public nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata callback, bytes4, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 id)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        id = keccak256(abi.encode(address(this), ++nonce));\n        _callbacks[id] = callback;\n    }\n\n    /// @dev Mirrors the real Intake: a reverting callback is swallowed, not retried.\n    function deliver(bytes32 id, OracleAttestation.Attestation calldata a, bytes calldata signature)\n        external\n        returns (bool ok)\n    {\n        Callback memory cb = _callbacks[id];\n        (ok,) = cb.target.call{gas: 200_000}(abi.encodeWithSelector(cb.selector, id, a, signature));\n    }\n}\n\n/// @notice A signed answer of zero dollars is the oracle's \"column unavailable\" failure mode, which the body's own\n/// definitions forbid. The consumer stores it as a real price, so one such answer makes every healthy loan of the\n/// class liquidatable at a 120%-of-debt opening price.\ncontract ZeroPriceLiquidationProof is Test {\n    uint256 internal constant SIGNER_KEY = 0xA11CE;\n    address internal alice = makeAddr(\"alice\");\n    address internal bob = makeAddr(\"bob\");\n    address internal lender = makeAddr(\"lender\");\n    MockUSDC internal usdc;\n    MockCardNFT internal nft;\n    CardPool internal pool;\n    CardOracle internal oracle;\n    CardLending internal lending;\n    ProofIMD2 internal imd;\n    ProofIntake2 internal intake;\n    bytes32 internal cardClass;\n    uint256 internal tokenId;\n    uint256 internal oracleNonce;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        usdc = new MockUSDC(address(this));\n        nft = new MockCardNFT(address(this));\n        pool = new CardPool(address(this), usdc);\n        imd = new ProofIMD2();\n        intake = new ProofIntake2();\n        oracle = new CardOracle(address(this), address(intake), address(imd), vm.addr(SIGNER_KEY));\n        lending = new CardLending(address(this), nft, pool, oracle, usdc);\n        pool.setLending(address(lending));\n        MockCardNFT.Card memory card = MockCardNFT.Card(\"Pokemon\", \"Pokemon Base Set\", \"Charizard\", \"4\", \"PSA\", \"10\", \"1\");\n        cardClass = nft.computeClassId(card);\n        tokenId = nft.mint(alice, card);\n        oracle.setClass(\n            cardClass, CardOracle.Template(\"Charizard\", \"4\", \"Pokemon Base Set\", \"pokemon-base-set\", \"charizard-4\", \"PSA 10\"), true\n        );\n        imd.mint(alice, 100 ether);\n        usdc.mint(lender, 10_000e6);\n        usdc.mint(bob, 10_000e6);\n        vm.startPrank(alice);\n        imd.approve(address(oracle), type(uint256).max);\n        nft.setApprovalForAll(address(lending), true);\n        vm.stopPrank();\n        vm.startPrank(lender);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lender);\n        vm.stopPrank();\n        vm.prank(bob);\n        usdc.approve(address(lending), type(uint256).max);\n        lending.setAPR(0);\n    }\n\n    function deliverPrice(uint256 dollars) internal returns (bool ok) {\n        vm.prank(alice);\n        bytes32 id = oracle.requestPrice(cardClass);\n        OracleAttestation.Attestation memory a;\n        a.requestId = keccak256(abi.encode(\"oracle UUID\", ++oracleNonce));\n        a.chainId = 1;\n        a.questionHash = keccak256(\"canonical question document\");\n        a.answerType = 3;\n        a.answer = abi.encode(dollars);\n        a.figure = dollars;\n        a.fromBlock = 100;\n        a.toBlock = 900;\n        a.blockHash = keccak256(\"closing block\");\n        a.panelJobId = keccak256(\"panel UUID\");\n        a.panelSize = 30;\n        a.quorum = 21;\n        a.agreed = 25;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 1 days);\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, oracle.attestationDigest(a));\n        ok = intake.deliver(id, a, abi.encodePacked(r, s, v));\n    }\n\n    function test_zeroDollarAnswerLiquidatesHealthyLoanAtOpeningPrice() public {\n        // A $1000 card secures a 450 USDC loan: 45% LTV, nowhere near the 60% liquidation line.\n        assertTrue(deliverPrice(1000));\n        vm.prank(alice);\n        uint256 loanId = lending.borrow(tokenId, 450e6);\n\n        // The panel answers 0 for the class (the \"unavailable\" reading the definitions tell it never to give).\n        vm.warp(block.timestamp + 1);\n        deliverPrice(0);\n\n        // Expected: no price means no price-triggered liquidation. Actual: 0 is a price and 450 > 60% of 0.\n        vm.expectRevert();\n        lending.liquidate(loanId);\n    }\n\n    function test_zeroDollarAnswerLetsAnyoneTakeTheCardFor540() public {\n        assertTrue(deliverPrice(1000));\n        vm.prank(alice);\n        uint256 loanId = lending.borrow(tokenId, 450e6);\n        vm.warp(block.timestamp + 1);\n        deliverPrice(0);\n        (bool liquidated,) = address(lending).call(abi.encodeWithSelector(CardLending.liquidate.selector, loanId));\n        if (liquidated) {\n            vm.prank(bob);\n            lending.buy(loanId, 540e6, bob);\n        }\n        // Expected: Alice's healthy collateral stays in custody. Actual: Bob holds the $1000 card for 540 USDC.\n        assertEq(nft.ownerOf(tokenId), address(lending), \"healthy loan collateral was auctioned on a zero answer\");\n    }\n}","reproduction":"Price $1000 delivered; Alice borrows 450e6 against tokenId (45% LTV). Warp +1s; requestPrice(classId) by anyone (0.5 IMD) and the panel answers 0: onPrice succeeds, prices[classId] = {0, now, now+1d}. Call liquidate(loanId) from any address: expected revert (no usable price, loan is healthy); actual: state -> Auction, auctionDebt 450e6, auctionPrice 540e6. Bob calls buy(loanId, 540e6, bob) in the same block: Bob owns a $1000 card for 540 USDC, pool receives 450, Alice's surplus is 90. Alice's net outcome 445.5 + 90 = 535.5 USDC for a $1000 card she never defaulted on. The proof's test_zeroDollarAnswerLiquidatesHealthyLoanAtOpeningPrice fails with 'next call did not revert as expected' and test_zeroDollarAnswerLetsAnyoneTakeTheCardFor540 fails with the NFT owned by Bob instead of CardLending.","severity":"medium","snippet":"        if (dollars > MAX_USD) revert BadAttestation();","title":"A signed zero-dollar answer is stored as a real price and liquidates every healthy loan in the class at a 120%-of-debt opening price"},{"citation":"resolved","description":"maxRedeem() returns min(balanceOf(holder), convertToShares(availableLiquidity())) with no regard to the dust floor that redeem() enforces at src/CardPool.sol:96 (`if (shares == 0 || previewRedeem(shares) == 0) revert InvalidAmount();`). With 12-decimal shares and a 6-decimal asset, any balance below one share-per-asset-unit (about 1e6 shares at par) previews to 0 assets, so the maximum the vault advertises is exactly an amount the vault refuses. ERC-4626 requires maxRedeem to return the largest amount that would not revert, and integrators (routers, aggregators) call redeem(maxRedeem(owner)). The same divergence exists for maxWithdraw() when it returns 0 while withdraw(0) reverts. Impact is dust-level (the stranded value is below 1e-6 USDC per holder) but the interface promise is false for any holder that receives a small share transfer. Fix: in maxRedeem return 0 when previewRedeem(result) == 0, and mirror it in maxWithdraw, or drop the previewRedeem == 0 refusal in redeem().","line":69,"path":"src/CardPool.sol","reproduction":"Fresh pool; lender deposits 10,000e6 (10,000e12 shares) and transfers 100000 shares (1e5) to bob. pool.maxRedeem(bob) == 100000 and pool.previewRedeem(100000) == 0. bob calls pool.redeem(100000, bob, bob): expected (ERC-4626) success, actual revert CardPool.InvalidAmount. Verified with a scratch test (test_redeemAtMaxRedeemReverts passes with vm.expectRevert(CardPool.InvalidAmount.selector)).","severity":"low","snippet":"    function maxRedeem(address holder) public view override returns (uint256) {\n        return Math.min(balanceOf(holder), convertToShares(availableLiquidity()));\n    }","title":"ERC-4626 max* guarantee broken for dust holders: redeem(maxRedeem(holder)) reverts"},{"citation":"resolved","description":"The protocol's own consumer base (src/OracleAttestation.sol:128-131, ISSUED_AT_TOLERANCE = 5 minutes) states that the attester stamps issuedAt with its wall clock while block.timestamp is the proposer's, and tolerates that drift. _settle() adds `a.issuedAt > block.timestamp` with zero tolerance. The Intake calls onPrice once under a try with 200k gas and does not retry a reverted callback, and requestPrice() refuses a second request while the first is pending (ClassBusy), so a single skewed delivery means: the requester's 0.5 IMD is spent, no price lands, borrow/extend/price-liquidation for that class stay blocked, and recovery needs the owner to bindRelay() plus someone to submit() before expiry, or a two-day clearExpired() wait. The README documents the stricter check as deliberate, so this is a liveness trade-off rather than a logic error, but the failing input is realistic and the cost is borne by the unprivileged requester. Fix: allow issuedAt up to block.timestamp + ISSUED_AT_TOLERANCE (and keep the other windows), matching the base.","line":260,"path":"src/CardOracle.sol","reproduction":"ask() opens request id; attestation with issuedAt = block.timestamp + 1 (all other fields valid, correctly signed for this consumer). Intake delivers onPrice(id, a, sig): actual revert BadAttestation (the repository's own test_expiryAndFutureTimestampRejected asserts this); expected per the base library: accepted as within tolerance. After the swallowed revert, pendingForClass[classId] == id, requestPrice(classId) reverts ClassBusy, freshPrice(classId) reverts StalePrice, and imd.balanceOf(requester) is 0.5 IMD lower with nothing stored.","severity":"low","snippet":"        if (\n            a.chainId != r.observationChainId || a.requestId == bytes32(0) || a.questionHash == bytes32(0)\n                || a.fromBlock > a.toBlock || a.issuedAt < r.openedAt || a.issuedAt > block.timestamp\n                || a.expiresAt < a.issuedAt || a.expiresAt > uint256(a.issuedAt) + MAX_AGE\n                || block.timestamp - a.issuedAt > MAX_AGE || a.answer.length != 32\n        ) revert BadAttestation();","title":"_settle rejects any attestation whose issuedAt is ahead of block.timestamp by even one second; a normal attester clock skew burns the 0.5 IMD fee and locks the class until the owner relays or two days"},{"citation":"resolved","description":"This is the design the brief asked for ('24-hour Dutch auction from 120% of debt down to 50%') and the README states a 1% reserve cannot cover a 50% outcome, so it is recorded as an economic trust assumption rather than a defect. The numbers matter for the judge: the pool's recovery depends entirely on competitive buyers appearing within the 24-hour curve, there is no keeper reward for liquidate(), and the borrower is an eligible buyer of their own collateral. On a thin market (a new deployment, a mock NFT on Sepolia, an illiquid class) the borrower's dominant strategy is to default.","line":214,"path":"src/CardLending.sol","reproduction":"Alice borrows 450 against a $1000 card and receives 445.5. She never repays; at maturity (debt 454.44 at 12% APR) anyone (or Alice) calls liquidate(). No third party buys within 24 h. Alice calls buy(loanId, 227.22e6, alice) at the floor: she holds the card again, net +218.28 USDC; pool.settle(227.22, 454.44) uses the 4.5 reserve and lenders lose 222.72. Repeated across loans the reserve (1% of principal) covers about 2% of each such shortfall.","severity":"info","snippet":"        uint256 elapsed = Math.min(block.timestamp - l.auctionStartedAt, AUCTION_DURATION);\n        uint256 numerator = 12_000 * AUCTION_DURATION - 7_000 * elapsed;\n        return Math.mulDiv(l.auctionDebt, numerator, BPS * AUCTION_DURATION, Math.Rounding.Ceil);","title":"Trust assumption, documented but material: the Dutch auction floor at 50% of debt makes a strategic default profitable whenever no third party bids within 24 hours, and the 1% reserve cannot absorb it"}],"hash":"47bbbe701ac3ceaacbcc287adb9c30afbaf54da16581c6417584aaaf6d79c957","nodeId":"5f9cc40a-82b9-48c4-b97f-e3e27177c514","outcome":"completed","summary":"The review is complete. The findings file is written and validated, both proofs fail on the current code for the stated reasons, and nothing outside `test/scratch/` and `.imd-findings.json` was touched.\n\n**What I audited.** The economics of CardVault against the Pashov Economic Security, Invariant and Flow Gap guides: value flows through borrow, repay, extend, liquidate, buy and settle; the NAV and reserve accounting of the ERC-4626 pool; interest accrual conservation; Dutch auction boundaries; the oracle's request, callback, failure and relay flows as they feed liquidation. I also posted the exact on-chain oracle body to the live check endpoint and confirmed from the public docs that `guards` and `toleranceBps` are valid paid-body keys, so the body itself is sound.\n\n**Findings written to `.imd-findings.json`** (5 findings, 65 coverage rows covering all 60 entry points, none unreached):\n\n1. **Medium, with proof.** Pool NAV carries defaulted auction debt at face value until a sale, and nothing ever writes it down. A lender exiting during an unsold auction receives 10,000.00 against a fair 9,889.75, leaving the entire loss on whoever stays. New depositors buy shares backed by a dead receivable. Suggested fix: value auction loans at the current Dutch price and add a bounded write-off.\n2. **Medium, with proof.** A signed zero-dollar answer, the oracle's \"unavailable\" reading that the body's own definitions forbid, is stored as a real price. One such answer makes every healthy loan of the class liquidatable at a 120%-of-debt opening price; in the proof Bob takes a $1000 card for 540 USDC.\n3. **Low.** `redeem(maxRedeem(holder))` reverts for dust holders, breaking the ERC-4626 max* promise. Verified with a scratch test.\n4. **Low.** The settle path refuses any attestation issued one second ahead of the block, with zero tolerance, while the protocol base allows five minutes. A normal clock skew burns the fee and locks the class until the owner relays. Documented as deliberate, so rated low.\n5. **Info.** The 50% auction floor and 1% reserve make strategic default profitable without a third-party bidder within 24 hours. This is the requested design, recorded as a trust assumption with numbers.\n\n**What held.** Interest accounting conserves exactly across all paths, the reserve and cash invariants hold, surplus is fully backed, reentrancy and pause interactions behave as documented, and the request and callback guards reject wrong senders, replays, weak panels and tampered answers.","treeHash":null,"usage":{"cachedInputTokens":2281527,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":83017,"runtime":"claude","turns":52,"wallClockMs":1212032}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ea62f3bebece9304","findings":[{"citation":"resolved","description":"liquidate() moves the loan's debt into frozenDebt (CardLending.sol:201 `frozenDebt += debt;`) and totalDebt() reports it at 100% until buy() or repay() closes it. CardPool.totalAssets() (CardPool.sol:52) adds totalDebt(), so share price ignores that the collateral was just repriced below 60% LTV and the auction may clear as low as 50% of debt. withdraw/redeem have no auction-aware limit. A lender who exits between liquidate() and buy() is paid at par, and the realised loss (debt - proceeds - reserve) is booked only against the shares left behind. The same applies to an auction nobody buys: it sits at par in NAV indefinitely. The loss is redistributed between lenders rather than shared pro rata, which breaks the ERC-4626 fairness guarantee the pool claims.","line":148,"path":"src/CardLending.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\nimport {MockCardNFT} from \"src/MockCardNFT.sol\";\nimport {CardPool} from \"src/CardPool.sol\";\nimport {CardOracle} from \"src/CardOracle.sol\";\nimport {CardLending} from \"src/CardLending.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ScratchIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\ncontract ScratchIntake {\n    uint256 public nonce;\n    mapping(bytes32 => IIntake.Callback) internal cbs;\n    function priceOf(bytes32, address) external pure returns (uint256) { return 0.5 ether; }\n    function requestWithFailure(bytes32, bytes calldata, IIntake.Callback calldata cb, bytes4, address asset, uint256 amount)\n        external payable returns (bytes32 id)\n    {\n        IERC20(asset).transferFrom(msg.sender, address(this), amount);\n        id = keccak256(abi.encode(++nonce));\n        cbs[id] = cb;\n    }\n    function deliver(bytes32 id, OracleAttestation.Attestation calldata a, bytes calldata sig) external {\n        address t = cbs[id].target; bytes4 s = cbs[id].selector;\n        (bool ok, bytes memory r) = t.call{gas: 200_000}(abi.encodeWithSelector(s, id, a, sig));\n        if (!ok) assembly { revert(add(r, 32), mload(r)) }\n    }\n}\n\ncontract AuctionNavTest is Test {\n    uint256 constant KEY = 0xA11CE;\n    MockUSDC usdc; MockCardNFT nft; CardPool pool; CardOracle oracle; CardLending lending;\n    ScratchIMD imd; ScratchIntake intake;\n    bytes32 cls; uint256 tokenId; uint256 n;\n    address alice = makeAddr(\"alice\"); address early = makeAddr(\"early\"); address late = makeAddr(\"late\");\n    address buyer = makeAddr(\"buyer\");\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        usdc = new MockUSDC(address(this));\n        nft = new MockCardNFT(address(this));\n        pool = new CardPool(address(this), usdc);\n        imd = new ScratchIMD();\n        intake = new ScratchIntake();\n        oracle = new CardOracle(address(this), address(intake), address(imd), vm.addr(KEY));\n        lending = new CardLending(address(this), nft, pool, oracle, usdc);\n        pool.setLending(address(lending));\n        MockCardNFT.Card memory c = MockCardNFT.Card(\"Pokemon\", \"Pokemon Base Set\", \"Charizard\", \"4\", \"PSA\", \"10\", \"1\");\n        cls = nft.computeClassId(c);\n        tokenId = nft.mint(alice, c);\n        oracle.setClass(cls, CardOracle.Template(\"Charizard\", \"4\", \"Pokemon Base Set\", \"pokemon-base-set\", \"charizard-4\", \"PSA 10\"), true);\n        imd.mint(alice, 10 ether);\n        vm.prank(alice); imd.approve(address(oracle), type(uint256).max);\n        vm.prank(alice); nft.setApprovalForAll(address(lending), true);\n        address[2] memory ls = [early, late];\n        for (uint256 i; i < 2; ++i) {\n            usdc.mint(ls[i], 1_000e6);\n            vm.startPrank(ls[i]); usdc.approve(address(pool), type(uint256).max); pool.deposit(1_000e6, ls[i]); vm.stopPrank();\n        }\n        usdc.mint(buyer, 10_000e6);\n        vm.prank(buyer); usdc.approve(address(lending), type(uint256).max);\n    }\n\n    function price(uint256 dollars) internal {\n        vm.prank(alice);\n        bytes32 id = oracle.requestPrice(cls);\n        OracleAttestation.Attestation memory a;\n        a.requestId = keccak256(abi.encode(\"uuid\", ++n));\n        a.chainId = 1; a.questionHash = keccak256(\"q\"); a.answerType = 3; a.answer = abi.encode(dollars);\n        a.fromBlock = 1; a.toBlock = 2; a.panelSize = 30; a.quorum = 21; a.agreed = 25;\n        a.issuedAt = uint64(block.timestamp); a.expiresAt = uint64(block.timestamp + 1 days);\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(KEY, oracle.attestationDigest(a));\n        intake.deliver(id, a, abi.encodePacked(r, s, v));\n    }\n\n    /// Two lenders with equal shares. The auctioned loan is carried at par, so the one who exits\n    /// between liquidate() and buy() takes full value and the other absorbs the whole shortfall.\n    function test_auctionedDebtCarriedAtParLetsEarlyExitDumpLoss() public {\n        price(1000);\n        vm.prank(alice);\n        uint256 loanId = lending.borrow(tokenId, 450e6); // 45% of $1,000\n        vm.warp(block.timestamp + 1 hours);\n        price(100); // card crashes: debt 450 > 60% of 100\n        lending.liquidate(loanId);\n\n        // Early lender exits at the par NAV while the card is at auction.\n        uint256 shares = pool.balanceOf(early);\n        vm.prank(early);\n        uint256 earlyOut = pool.redeem(shares, early, early);\n\n        vm.warp(block.timestamp + 1 days);\n        vm.prank(buyer);\n        lending.buy(loanId, type(uint256).max, buyer); // clears at the 50% floor\n\n        uint256 lateOut = pool.previewRedeem(pool.balanceOf(late));\n        emit log_named_uint(\"early lender received\", earlyOut);\n        emit log_named_uint(\"late lender holds   \", lateOut);\n        // Equal shares, same loan: exiting during the auction must not let one lender escape the loss.\n        assertLe(earlyOut, lateOut + 1e6, \"early exit escaped the auction loss\");\n    }\n}","reproduction":"Two lenders each deposit 1,000 USDC (equal shares). The oracle prices the class at $1,000 and alice borrows 450 USDC. One hour later a new attestation prices it at $100, and liquidate(1) starts an auction for 450 debt. The early lender immediately calls redeem(allShares) and receives 1,000.003081 USDC (par plus interest). After 24h a buyer calls buy(1, max, buyer) at the 50% floor (225 USDC); the reserve covers 4.5. The late lender's shares are now worth 779.500001 USDC. Expected: both lenders carry about 110 USDC of loss each, since the auctioned receivable should be marked conservatively (for example at the guaranteed floor plus reserve) or exits limited while auctions are open. Actual: the early lender loses nothing and the late lender loses 220.5. The proof test fails with `1000003081 > 780500001`.","severity":"medium","snippet":"        return activePrincipal + frozenDebt + (interestNumerator + rateWeight * (block.timestamp - lastAccrual))","title":"Auctioned loans stay in pool NAV at full debt, so lenders who exit during an auction move the whole shortfall onto those who stay"},{"citation":"resolved","description":"extend() collects the accrued interest in the same call (`_payPool(msg.sender, interest, interest)`), so after extension the debt is just l.principal. The LTV check still adds that interest. The brief asks for 'extend (new attestation, same LTV check)', which is borrow's `amount <= 45% of price`. A borrower who drew the advertised 45% maximum fails the check at any price that has not risen, even though their renewed debt would be exactly 45%.","line":176,"path":"src/CardLending.sol","reproduction":"Class priced at $1,000; alice calls borrow(tokenId, 450e6) (exactly 45%). warp 29 days; a new attestation again prices the class at $1,000 (newer issuedAt); alice holds enough USDC and has approved lending. alice.extend(1) reverts TooMuchDebt: 450 + ~4.29 interest > 450. Expected: the extension succeeds because the post-extension debt is 450 = 45% of 1,000. Fix: compare l.principal (the debt after the interest is paid) with the 45% cap.","severity":"low","snippet":"        if (l.principal + interest > Math.mulDiv(p.price, MAX_LTV_BPS, BPS)) revert TooMuchDebt();","title":"extend() checks principal + already-paid interest against 45% LTV, so a loan at the 45% limit can never be extended at an unchanged price"},{"citation":"resolved","description":"_verifyAttestation deliberately allows issuedAt up to ISSUED_AT_TOLERANCE (5 minutes) ahead of block.timestamp, because the attester stamps issuedAt with its own wall clock while the block's timestamp is the validator's slot time. _settle then rejects any issuedAt > block.timestamp. If the writer's transaction lands in a block whose slot timestamp is even 1 second earlier than the attester's clock (for example when the block is proposed late in its slot), the callback reverts. The Intake records that as not delivered and does not retry. requests[id].pending and pendingForClass stay set, so requestPrice for the class reverts ClassBusy until the owner manually calls bindRelay plus submit or REQUEST_TIMEOUT (2 days) passes. Meanwhile no borrower in the class can get the newer price that extend() needs, and the 0.5 IMD is spent.","line":260,"path":"src/CardOracle.sol","reproduction":"requestPrice(cls) -> intakeId. The Intake delivers onPrice(intakeId, a, sig) with a valid signature, panel 30/21/25, a.issuedAt = block.timestamp + 1 and expiresAt = issuedAt + 1 day. Actual: reverts BadAttestation (confirmed in a scratch test). A later requestPrice(cls) reverts ClassBusy. Expected: accept within the library's 5-minute tolerance, or clamp, as the reference consumer does.","severity":"low","snippet":"                || a.fromBlock > a.toBlock || a.issuedAt < r.openedAt || a.issuedAt > block.timestamp","title":"onPrice rejects an attestation issued up to 5 minutes ahead of block time; the callback is not retried and the class stays locked for 2 days"},{"citation":"resolved","description":"_settle bounds the answer only from above. The request's definitions say 'If unavailable, report inability, not zero', so 0 is never a legitimate card price. A signed 0 (for example a panel reading an empty '-' column as 0) is stored as Price(0, issuedAt, expiresAt). freshPrice returns it, and liquidate() treats any debt > 0 as above 60% LTV, so every active loan in the class can be pushed into auction before maturity.","line":266,"path":"src/CardOracle.sol","reproduction":"Class priced at $1,000; alice borrows 100 USDC (10% LTV). warp 1h; a correctly signed attestation for a new request answers abi.encode(uint256(0)). Anyone calls liquidate(1): it succeeds and the loan enters State.Auction (confirmed in a scratch test). Expected: _settle rejects dollars == 0 (keeping the request pending or clearing it as a failure), so a 10%-LTV loan is not liquidated.","severity":"low","snippet":"        if (dollars > MAX_USD) revert BadAttestation();","title":"A zero-dollar answer is stored as a valid fresh price, making every loan in the class immediately liquidatable"}],"hash":"6afeef8ddd7f522725a19766bb0f2c2d4c6359e1aaddab61d9a7d618a2c0eb9c","nodeId":"7fbeedbc-81f6-4750-92b6-9a15ef7c6b2e","outcome":"completed","summary":"I found four defects in CardVault: one medium and three low, each confirmed by a test that fails on the current code. They are in `.imd-findings.json`, which also has 62 coverage rows: all 60 listed entry points plus two checks of my own. I changed no source files. The project's own 70 tests all pass.\n\n**Findings**\n\n1. **Medium: a loan at auction still counts at full value in the lending pool's share price** (`src/CardLending.sol:148`).\n   - **How it happens:** when a loan is liquidated, the pool keeps valuing it at the full debt until someone buys the card or repays. A lender who withdraws in that gap is paid in full. Whatever the auction fails to recover then falls entirely on the lenders who stayed.\n   - **Example:** two lenders deposit 1,000 USDC each. A card priced at $1,000 backs a 450 USDC loan, and its price then drops to $100. The lender who exits during the auction gets 1,000.003 USDC; the one who stays is left with 779.50.\n   - **Proof:** the full test is attached to the finding.\n   - **Possible fix:** value auctioned debt conservatively (for example at the auction's 50% floor), or limit withdrawals while an auction is open.\n2. **Low: `extend` wrongly blocks loans at the 45% limit** (`src/CardLending.sol:176`). The check adds interest that the same call pays off, so it is stricter than the check at borrowing. A 450 USDC loan against a $1,000 card cannot be extended after 29 days at the same $1,000 price.\n3. **Low: price callbacks can be rejected for a timestamp only 1 second ahead of the block's time** (`src/CardOracle.sol:260`).\n   - **Cause:** the shared attestation library allows a 5-minute lead for clock differences, but `CardOracle` rejects any lead at all.\n   - **Impact:** the Intake does not retry a rejected callback, so no new price can be requested for that card class for 2 days unless the owner steps in. Borrowers in that class cannot extend in that time.\n4. **Low: a $0 answer is stored as a valid price** (`src/CardOracle.sol:266`). Every loan in that card class then becomes liquidatable before maturity. In the test, a loan at 10% loan-to-value went straight to auction. The request text itself tells the oracle panel never to answer 0.\n\n**Coverage**\n\n- **My guides (Execution Trace, Periphery, First Principles):** I traced every entry point in `CardLending`, `CardPool`, `CardOracle`, the ERC-4626 base, the attestation library, `Admin`/`Guarded` and both mock tokens. I marked none of the 60 entry points as unreached.\n- **Checked and found sound:**\n  - interest bookkeeping across borrow, extend, repay and liquidate;\n  - the 200,000-gas limit on callbacks (the most `onPrice` used in the tests was 148,000);\n  - Intake changes while a request is still open;\n  - replay protection and the relayed-delivery binding;\n  - the cleanup and approval reset in `requestPrice`.\n- **Static-analysis leads I set aside:** the reentrancy warnings on `requestPrice` and `borrow` only involve calls to trusted contracts and are guarded. The \"strict equality\" warnings on `deposit`/`redeem` are just zero-amount checks.\n- **Not done:** I did not test the request body against the live `/requests/check` service, and I ran no fork tests.","treeHash":null,"usage":{"cachedInputTokens":1522940,"inputTokens":34,"model":"claude-opus-5-5","outputTokens":32442,"runtime":"claude","turns":20,"wallClockMs":365212}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7c0191a32541eb74","findings":[{"citation":"resolved","description":"Access x asymmetry seam. borrow() and extend() both snapshot the global aprBps, but extend() has no parameter letting the borrower bound the rate they accept, and the owner may move aprBps anywhere in 0..10000 at any time with setAPR(). A borrower who signs extend(id) under the published 12% can be renewed at up to 100% APR for the full 30-day term if setAPR() is mined before their transaction (ordinary admin parameter change in the same block, not only a malicious one). The borrower's only remedy is to repay in full; the loan cannot be re-extended at a lower rate until a strictly newer attestation exists. Suggested minimal fix that keeps the owner-set rate: add a maxAprBps argument to extend() and revert if aprBps > maxAprBps (and emit the rate in the Extended event, which it already does).","line":180,"path":"src/CardLending.sol","reproduction":"State: alice borrowed 400e6 at aprBps 1200 against a 1000 USD price; 10 days pass; a fresh attestation is delivered. Sequence in one block: owner calls setAPR(10000); alice calls extend(id). Expected (from the borrower's view): renewal at the 12% she saw, or a revert she can inspect. Actual: extend succeeds and loan(id).aprBps == 10000; interest for the new term accrues at 100% APR. Verified with a Foundry test: after the sequence assertEq(lending.loan(id).aprBps, 10_000) holds and no revert occurs.","severity":"low","snippet":"        l.aprBps = aprBps;","title":"extend() snapshots the live owner APR with no borrower-side bound, so a setAPR that lands first silently reprices the renewal"},{"citation":"resolved","description":"Over-restrictive check (Asymmetry step 6, bad symmetry). OracleAttestationConsumer._verifyAttestation already enforces issuedAt <= block.timestamp + ISSUED_AT_TOLERANCE (5 minutes) precisely because the attester stamps with its own wall clock and the block timestamp is a validator's. _settle then adds a strict a.issuedAt > block.timestamp rejection. The writer signs and broadcasts within the same slot, so a signer clock a few seconds ahead of the proposer's slot time produces a signed, otherwise valid attestation that the callback rejects with BadAttestation. The Intake attempts the callback once and does not retry; the 0.5 IMD is spent, prices[classId] is not written, and pendingForClass stays set so no one can requestPrice for that class until clearExpired succeeds after REQUEST_TIMEOUT (2 days). Borrow/extend for that class stall meanwhile. The README documents the choice but the cost is borne by whoever paid. Minimal fix: drop the strict clause and rely on the inherited tolerance (freshPrice already rejects p.issuedAt > block.timestamp at read time), or allow a small bounded tolerance such as 5 minutes consistent with the library.","line":260,"path":"src/CardOracle.sol","reproduction":"State: class allowlisted, alice holds 100 IMD. Sequence: alice calls requestPrice(classId) (fee 0.5 IMD pulled); Intake delivers onPrice(intakeId, a, sig) where a is correctly signed by the configured signer with a.issuedAt = block.timestamp + 1, a.expiresAt = a.issuedAt + 1 days, panel 30/21/25, chainId 1, answer abi.encode(1000). Expected: the attestation is inside the protocol's tolerance window and should be stored (or at least not leave the class locked). Actual: the callback returns false with revert data BadAttestation; request(intakeId).pending stays true; alice's IMD balance is 99.5; a second requestPrice(classId) reverts ClassBusy; clearExpired(intakeId) reverts TooEarly until block.timestamp > openedAt + 2 days. Verified with a Foundry test using a mock Intake that forwards the call under 200,000 gas.","severity":"low","snippet":"                || a.fromBlock > a.toBlock || a.issuedAt < r.openedAt || a.issuedAt > block.timestamp","title":"_settle refuses any issuedAt later than block.timestamp, discarding the library's 5-minute drift tolerance; a 1-second clock skew burns the fee and locks the class for two days"},{"citation":"resolved","description":"Trust-assumption note, not a bypass. Guarded sets guardian = admin in the constructor and only setGuardian (onlyOwner) changes it. After transferOwnership + acceptOwnership on CardLending, CardPool and CardOracle, the former owner remains guardian on each and can pause() them (blocking borrow, extend, liquidate, deposit/mint/draw and requestPrice) while only the new owner can unpause. The README runbook (step 4) tells the operator to rotate guardians explicitly, so this is recorded as a documented trust assumption for the judge rather than a defect; it would become a finding only if the handover is expected to fully strip the old admin.","line":13,"path":"src/Guarded.sol","reproduction":"Sequence: owner A calls transferOwnership(B) on lending, pool and oracle; B calls acceptOwnership() on each (owner() == B on all three). A then calls lending.pause(), pool.pause(), oracle.pause(). Expected under a strict reading of 'two-step admin hand-over': A has no remaining power. Actual: all three pause() calls succeed (paused() == true on each) and A's unpause() reverts OwnableUnauthorizedAccount, so only B can resume. Verified with a Foundry test.","severity":"info","snippet":"        guardian = admin;","title":"Guardian is initialised to the deployer-admin and does not follow the two-step ownership handover, so the previous owner keeps pause power on all three guarded contracts"}],"hash":"7ff0bfbdcf1f30372d28f8a8f32df013a7b4d5019401741073eb429bd23d50ca","nodeId":"0e676485-6d05-45ff-a18e-62e354fb3445","outcome":"completed","summary":"The review is complete. The findings file is written and validated, and the working tree is otherwise unchanged.\n\n**Outcome:** the permission model of CardVault holds up. Every state-changing entry point was traced for its guard, its mirrored state writes and its failure paths, and I found no permission bypass, no confused-deputy path, no unguarded writer of a guarded variable, and no asymmetric pair that leaks value. The three items reported are low or informational.\n\n**Findings written to `.imd-findings.json`:**\n\n1. **Low, `extend` has no rate bound.** Renewal snapshots whatever `aprBps` is current, and the owner may set it anywhere up to 100%. A borrower whose `extend` lands after a `setAPR` is silently repriced for the full 30-day term. Reproduced: after `setAPR(10000)` then `extend(id)`, the loan's rate reads 10000 with no revert.\n2. **Low, strict issuance clause in `_settle`.** The library allows five minutes of clock drift, but the consumer additionally rejects any `issuedAt` past `block.timestamp`. A signed attestation one second ahead is refused, the fee is spent, and the class stays busy for two days. Reproduced with a mock Intake under the 200,000 gas stipend.\n3. **Info, guardian survives the handover.** After a completed two-step ownership transfer, the previous owner remains guardian on all three guarded contracts and can pause them. The README runbook already instructs operators to rotate guardians, so this is recorded as a trust assumption rather than a defect.\n\n**Coverage:** all 60 listed entry points have a row, plus three invariants I checked. Two rows reference finding 1 or 2; the rest are `holds` with a one-line reason. None are `unreached`.\n\n**What I did not cover:** economics of the Dutch auction floor and the oracle body wording were outside my three guides, so they were left to the other specialists. The Sepolia fork test and the live `POST /requests/check` call were not run.","treeHash":null,"usage":{"cachedInputTokens":1373029,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":41603,"runtime":"claude","turns":40,"wallClockMs":600828}},{"artifacts":[],"attempt":1,"bundleHash":"36ca33521dfbdb25166440e09158a000fc4ae817d6c63e048e21401b6a7a0189","device":"fb26479062458645","findings":[],"hash":"86e6d086381200ef662ea72bcee0f5b61392b62733004e4b8d95e29858f99eda","nodeId":"fdadb89e-e85a-4d48-a26d-ea84e4c242e9","outcome":"completed","summary":"The manifest in `launch.json` is already correct against the accepted tree, so this revision changes nothing.\n\n**What I verified**\n\n- **Constructor signatures** match every manifest entry: MockUSDC(admin), MockCardNFT(admin), CardPool(admin, usdc), CardOracle(admin, intake, imd, signer), CardLending(admin, nft, pool, oracle, usdc). The merged work from the other contributor changed CardLending and CardPool internals but left their constructors unchanged.\n- **Static addresses** match the brief's Sepolia Intake and test IMD. The oracle signer `0x5598aa91…2982` is still the attester the public oracle API reports today.\n- **Admin and guardian** resolve to `$owner` on all five contracts. The Ownable constructor's ownership hook seats the admin as initial guardian, so the notes' guardian claim holds.\n- **Empty-chain rehearsal** in test/scratch deployed all five contracts with the exact manifest arguments under chain id 11155111 in one call and checked owner, guardian, token name C, symbol AUREON, the 1,000,000 supply, intake, signer and pool wiring. It passed.\n- **Full suite** with the verifier's fuzz seed: 87 tests passed, 0 failed. All runtimes are under the EIP-170 limit, the largest being CardOracle at 19,085 bytes.\n- **Schema shape**: only the keys kind, contracts and notes; five unique names under 32 characters; every argument a string; notes at 1,437 characters.\n\nNo file outside test/scratch was modified, and git status is clean. The notes already disclose the one accepted-source limitation: CardPool needs the owner's one-time setLending call and class configuration before borrowing works, since the manifest cannot carry initialization calls.","treeHash":"4c0a5fcee9ae3e86505619a54826239e4d8bcd9a","usage":{"cachedInputTokens":355465,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":5317,"runtime":"claude","turns":9,"wallClockMs":151797}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f9a12c57a094ca8d","findings":[{"citation":"resolved","description":"Settles and merges the four round-one specialist findings on auction debt carried at face value (428e780d, 3abd2234, 6c3d640d and the related sandwich I reported, 965d7546). The revision now marks each auction loan at min(debt, Dutch price) and writes it down linearly to zero after the floor, and every settlement is NAV-neutral: my earlier finding's cases A and B now cost exactly the 200e6 floor and the 400e6 debt with the lender's claim unchanged (reproduced), and proof 6c3d640d passes. Proof 428e780d no longer reproduces (it relies on a zero-dollar attestation, which _settle now rejects). Proof 3abd2234 still fails, for a narrower reason than it states: for the first PAR_WINDOW (24,685 s) after liquidate() the Dutch price is above the debt, so the mark is the full debt (`mark = b.parDebt;`, and `auctionMark` line 198 returns min(debt, price) = debt), and before anyone calls liquidate() an Active loan is carried at principal plus interest regardless of the stored price. When liquidation was triggered by a newer attestation that put LTV above 60%, the contract already holds a fresh price showing what the collateral is worth, yet the mark ignores it for almost seven hours. A lender who redeems in that window leaves at par and the remaining lenders carry the whole shortfall. This is a loss redistribution among lenders, bounded by available liquidity and by the window, not an extraction by an outsider, so it is low. A fix that keeps the design: when opening or marking a price-liquidated auction, cap the mark by the class's stored fresh price as well (min(debt, Dutch price, oracle price)), and apply the same cap in netReceivable to Active loans whose class has a fresh price below debt/0.6; note that any such cap moves NAV at the moment a price lands, so the trade-off should be stated in the README if the author keeps the current mark instead.","line":430,"path":"src/CardLending.sol","reproduction":"Two lenders each deposit 1,000e6 (equal shares). Price 1000 delivered; alice borrows 450e6 against tokenId. Warp +1 hour; price 100 delivered (450 > 60% of 100); anyone calls liquidate(loanId). Immediately (elapsed 0 <= PAR_WINDOW) the early lender calls redeem(all shares): receives 1,000,003,082 (par plus interest). Warp +1 day; buyer calls buy(loanId, max, buyer) at the 50% floor (225e6); the 4.5e6 reserve is spent. Late lender's previewRedeem(all shares) is 779,500,001. Expected: both lenders carry roughly the same share of the 220.5e6 loss (each about 889.75e6 on exit). Actual: early lender 1,000,003,082, late lender 779,500,001. The supplied proof .imd/reads/proofs/Proof_3abd2234d700.t.sol fails on the current code with 'early exit escaped the auction loss: 1000003082 > 780500001'. The same test with the early exit moved to elapsed > 24,685 s passes, which is why this is narrowed to the par window and the pre-liquidate() interval.","severity":"low","snippet":"        mark = b.parDebt;","title":"Residual of the round-one NAV findings: a price-liquidated loan is still marked at full debt for the first 24,685 s of its auction (and until liquidate() is called), so a lender who exits in that wind"},{"citation":"resolved","description":"Merges the independent tester's finding 79ed8a22 (reproduced). The NAV-neutral settlement that fixes the round-one sandwich holds back as reserve everything a payment collects above the loan's current mark, and the checkpointed cover lets that reserve count in NAV only against mark-downs that happen afterwards. Consequently a recovery on a loan whose loss lenders already carried never reaches those lenders: after a written-off 400e6 loan is bought at its 200e6 floor, or repaid in full by the borrower, the proceeds stay in `reserve`, are excluded from availableLiquidity, maxWithdraw and totalAssets, and no owner or lender function moves them. They are spent only on future shortfalls, benefiting whoever is a lender then; with no further auctions they stay in the contract after every lender has redeemed. The brief's waterfall says proceeds repay the pool; here they repay only future losses. The README discloses the trade-off and it is the consequence of the fix I asked for, so this is recorded as a design note for the judge, not a blocking defect. If the author wants a release, it must not be sandwichable: an owner-triggered release that moves NAV atomically would reopen the round-one finding, so any release should be streamed (linear over a period, like the write-off) rather than stepped.","line":166,"path":"src/CardPool.sol","reproduction":"Fixture: lender deposits 10,000e6; owner setAPR(0); price 1000; alice borrows 400e6 (reserve 4e6); warp +30 days; liquidate; warp +2 days (mark 0, totalAssets 9,604e6); bob calls buy(loanId, 200e6, bob). Actual: pool.reserve() == 200e6, totalAssets() == 9,604e6, maxWithdraw(lender) ~ 9,604e6; after the lender redeems everything the pool still holds 200e6 of USDC with totalSupply ~ 0 and a later depositor's shares are priced on the empty NAV, so the 200e6 accrues to nobody. Two-loan variant from my scratch run: two 400e6 loans liquidated at maturity, warp +20 h; alice repays loan 1 in full (payment 400e6 against a mark of about 246.67e6): reserve goes from 8e6 to 161,333,333 and totalAssets stays 9,701,333,334; bob then buys loan 2 at the 200e6 floor: reserve ends at 106,666,666, totalAssets 9,701,333,334, pool cash 9,808e6. 106.67e6 of collected proceeds are permanently outside lender NAV. See also test/CardSettlementEdges.t.sol::test_heldBackReserveIsNeverLenderCashAndHasNoRelease.","severity":"info","snippet":"        reserve = reserve - used + heldBack;","title":"Held-back settlement recoveries sit in the reserve with no release path, so lenders who carried a write-off never receive a later recovery on that loan"}],"hash":"aa34f9a0eaf6281c4d9057f6ef7eb250a8630767d50c57774031a41923caa291","nodeId":"e601e665-9ae0-4d2f-a02a-6d45305dce7e","outcome":"completed","summary":"Review complete. The findings file is written with two entries and a full coverage record, and the scratch tests are removed.\n\n**My earlier finding is fixed.** The settlement sandwich from round one no longer reproduces. I re-ran both cases from my report against the revised code:\n\n| Case | Expected | Actual now |\n| --- | --- | --- |\n| A: bob deposits 10,000, buys the written-off loan at the 200 floor, redeems | net cost 200e6 | 200,000,001 |\n| B: alice deposits 1,000,000, repays her auctioned 400 loan, redeems | net cost 400e6 | 400,000,001 |\n\nThe lender's claim stays at 9,604e6 in both cases. Fuzzing buy and repay at every point on the curve from 0 to 49 hours with a million-USDC sandwich takes nothing, and sampled NAV over the full curve never rises with time and has no cliff. The full suite of 162 tests passes.\n\n**Specialists' round-one findings settled.** Zero-dollar answers are rejected, the strict issuedAt check is gone, extend checks principal only and takes a rate bound, interest rounds up, maxRedeem agrees with redeem, and the guardian seat now follows ownership. Two of the four attached proofs still fail, but one relies on a zero price that is now refused, so it does not reproduce. The other does, in a narrower form.\n\n**Kept findings:**\n- **Low:** A loan liquidated on a price crash is still marked at full debt for the first 24,685 seconds of its auction, and until someone calls liquidate. The attached proof shows the early lender exiting at 1,000,003,082 while the late lender holds 779,500,001. This is loss redistribution between lenders inside a seven-hour window, so I rated it low and noted that any oracle-price cap on the mark would itself move NAV when a price lands.\n- **Info:** Held-back recoveries have no release path. This merges the tester's note. The 200 USDC floor sale of a written-off loan stays in reserve after every lender has left. It is the consequence of the fix I asked for, and the README discloses it.\n\nCoverage answers all 60 entry points plus three invariant rows. The manifest deploys: five contracts in dependency order, no constructor calls another contract, largest runtime 19,085 bytes.","treeHash":null,"usage":{"cachedInputTokens":1597460,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":33931,"runtime":"claude","turns":25,"wallClockMs":602234}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0cf632e317dfab7a","findings":[{"citation":"resolved","description":"totalAssets() = availableLiquidity() + CardLending.totalDebt(), and totalDebt() counts frozenDebt (auctioned loans) and activePrincipal plus accrued interest at 100% until pool.settle() is called from buy() or repay(). The auction price only reaches the 50% floor 24 hours after liquidate(), and a matured loan keeps accruing interest into NAV until someone calls liquidate(). During that window every share still prices in the full debt, so a lender who redeems (liquidity permitting) leaves at pre-loss NAV and the uncovered shortfall is realised entirely against the lenders who stayed. The same stale NAV lets new depositors buy in above fair value. Seam: boundary (auction window / matured loan) x invariant (pro-rata loss sharing). Minimal fix preserving the design: at liquidate() recognise the frozen loan in NAV at the auction floor (debt*50%) plus the reserve it can draw, or more simply haircut frozenDebt in totalDebt() to the current auctionPrice(); recognise the difference back on repay()/buy().","line":53,"path":"src/CardPool.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\nimport {MockCardNFT} from \"src/MockCardNFT.sol\";\nimport {CardPool} from \"src/CardPool.sol\";\nimport {CardOracle} from \"src/CardOracle.sol\";\nimport {CardLending} from \"src/CardLending.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ninterface IERC20Min {\n    function transferFrom(address from, address to, uint256 amount) external returns (bool);\n}\n\ncontract ProofIMD {\n    event Transfer(address indexed from, address indexed to, uint256 value);\n    event Approval(address indexed owner, address indexed spender, uint256 value);\n    string public constant name = \"Test IMD\";\n    string public constant symbol = \"IMD\";\n    uint8 public constant decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        totalSupply += amount;\n        balanceOf[to] += amount;\n        emit Transfer(address(0), to, amount);\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        emit Approval(msg.sender, spender, amount);\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        return _move(msg.sender, to, amount);\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        uint256 a = allowance[from][msg.sender];\n        if (a != type(uint256).max) allowance[from][msg.sender] = a - amount;\n        return _move(from, to, amount);\n    }\n\n    function _move(address from, address to, uint256 amount) private returns (bool) {\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        emit Transfer(from, to, amount);\n        return true;\n    }\n}\n\ncontract ProofIntake is IIntake {\n    struct Ask {\n        Callback callback;\n        bytes4 failure;\n    }\n\n    mapping(bytes32 => Ask) private _asks;\n    uint256 public nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata callback, bytes4 onFailure, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 id)\n    {\n        require(IERC20Min(asset).transferFrom(msg.sender, address(this), amount));\n        id = keccak256(abi.encode(address(this), ++nonce));\n        _asks[id] = Ask(callback, onFailure);\n    }\n\n    function deliver(bytes32 id, OracleAttestation.Attestation calldata a, bytes calldata signature)\n        external\n        returns (bool ok, bytes memory data)\n    {\n        Callback memory cb = _asks[id].callback;\n        (ok, data) = cb.target.call{gas: 200_000}(abi.encodeWithSelector(cb.selector, id, a, signature));\n    }\n}\n\n/// Two lenders with equal stakes. A loan defaults and goes to auction. Lender A redeems during the auction\n/// window and receives its full pre-loss NAV; the shortfall realised at `buy` falls entirely on lender B.\n/// Expected: both lenders bear the loss pro rata (each about half of the 220.5 USDC uncovered shortfall).\ncontract LossShiftProof is Test {\n    uint256 internal constant SIGNER_KEY = 0xA11CE;\n    address internal alice = makeAddr(\"alice\");\n    address internal bob = makeAddr(\"bob\");\n    address internal lenderA = makeAddr(\"lenderA\");\n    address internal lenderB = makeAddr(\"lenderB\");\n    MockUSDC internal usdc;\n    MockCardNFT internal nft;\n    CardPool internal pool;\n    CardOracle internal oracle;\n    CardLending internal lending;\n    ProofIMD internal imd;\n    ProofIntake internal intake;\n    bytes32 internal cardClass;\n    uint256 internal tokenId;\n    uint256 internal oracleNonce;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        vm.roll(1_000);\n        usdc = new MockUSDC(address(this));\n        nft = new MockCardNFT(address(this));\n        pool = new CardPool(address(this), usdc);\n        imd = new ProofIMD();\n        intake = new ProofIntake();\n        oracle = new CardOracle(address(this), address(intake), address(imd), vm.addr(SIGNER_KEY));\n        lending = new CardLending(address(this), nft, pool, oracle, usdc);\n        pool.setLending(address(lending));\n        lending.setAPR(0);\n        MockCardNFT.Card memory card = MockCardNFT.Card(\"Pokemon\", \"Pokemon Base Set\", \"Charizard\", \"4\", \"PSA\", \"10\", \"1\");\n        cardClass = nft.computeClassId(card);\n        tokenId = nft.mint(alice, card);\n        oracle.setClass(\n            cardClass, CardOracle.Template(\"Charizard\", \"4\", \"Pokemon Base Set\", \"pokemon-base-set\", \"charizard-4\", \"PSA 10\"), true\n        );\n        imd.mint(alice, 100 ether);\n        usdc.mint(bob, 100_000e6);\n        usdc.mint(lenderA, 10_000e6);\n        usdc.mint(lenderB, 10_000e6);\n        vm.startPrank(alice);\n        imd.approve(address(oracle), type(uint256).max);\n        nft.setApprovalForAll(address(lending), true);\n        vm.stopPrank();\n        vm.prank(bob);\n        usdc.approve(address(lending), type(uint256).max);\n        vm.startPrank(lenderA);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lenderA);\n        vm.stopPrank();\n        vm.startPrank(lenderB);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lenderB);\n        vm.stopPrank();\n    }\n\n    function deliverPrice(uint256 dollars) internal {\n        vm.prank(alice);\n        bytes32 id = oracle.requestPrice(cardClass);\n        OracleAttestation.Attestation memory a;\n        a.requestId = keccak256(abi.encode(\"oracle UUID\", ++oracleNonce));\n        a.chainId = 1;\n        a.questionHash = keccak256(\"canonical question document\");\n        a.answerType = 3;\n        a.answer = abi.encode(dollars);\n        a.figure = dollars;\n        a.fromBlock = 100;\n        a.toBlock = 900;\n        a.blockHash = keccak256(\"closing block\");\n        a.panelJobId = keccak256(\"panel UUID\");\n        a.panelSize = 30;\n        a.quorum = 21;\n        a.agreed = 25;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 1 days);\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, oracle.attestationDigest(a));\n        (bool ok, bytes memory reason) = intake.deliver(id, a, abi.encodePacked(r, s, v));\n        if (!ok) {\n            assembly (\"memory-safe\") {\n                revert(add(reason, 32), mload(reason))\n            }\n        }\n    }\n\n    function test_lenderExitingDuringAuctionDoesNotEscapeLoss() public {\n        deliverPrice(1000);\n        vm.prank(alice);\n        uint256 id = lending.borrow(tokenId, 450e6); // max LTV; fee 4.5e6 to reserve\n        vm.warp(block.timestamp + 1 hours);\n        deliverPrice(0); // card now priced at zero: liquidatable\n        lending.liquidate(id);\n        assertEq(pool.totalAssets(), 20_000e6, \"NAV unchanged at liquidation\");\n\n        // Lender A exits during the auction window.\n        uint256 sharesA = pool.balanceOf(lenderA);\n        vm.prank(lenderA);\n        uint256 gotA = pool.redeem(sharesA, lenderA, lenderA);\n\n        // Auction reaches its 50% floor and is bought; shortfall 225e6, reserve covers 4.5e6.\n        vm.warp(block.timestamp + 1 days);\n        vm.prank(bob);\n        lending.buy(id, 225e6, bob);\n        uint256 valueB = pool.previewRedeem(pool.balanceOf(lenderB));\n\n        uint256 uncovered = 225e6 - 4.5e6; // 220.5 USDC\n        // Each lender should bear about half of the uncovered shortfall.\n        assertLe(gotA, 10_000e6 - uncovered / 2 + 1e6, \"lender A escaped its share of the loss\");\n        assertGe(valueB, 10_000e6 - uncovered / 2 - 1e6, \"lender B bore more than its share of the loss\");\n    }\n}","reproduction":"Two lenders deposit 10,000 USDC each (NAV 20,000). APR 0 for clarity. Alice borrows 450 USDC against a $1000 card (fee 4.5 goes to reserve). One hour later an attestation at $0 lands; liquidate(id) succeeds; pool.totalAssets() is still 20,000e6. Lender A redeems all shares and receives 10,000e6. Warp +1 day; bob calls buy(id, 225e6, bob) at the 50% floor: settle(225e6, 450e6) uses 4.5e6 reserve, uncovered loss 220.5e6. Lender B's previewRedeem(balance) is now 9,779.5e6. Expected: each lender bears ~110.25e6; actual: A bears 0 and B bears 220.5e6. Scratch run: 'NAV at auction start: 20000000000 / lender redeemed: 10000000000 / carol value after: 9779500000'.","severity":"medium","snippet":"        return availableLiquidity() + (lending == address(0) ? 0 : ICardDebt(lending).totalDebt());","title":"Pool NAV carries defaulted and auctioned debt at face value, so a lender who exits before buy() shifts the whole shortfall onto remaining lenders"},{"citation":"resolved","description":"OracleAttestationConsumer._verifyAttestation (src/OracleAttestation.sol:161) explicitly allows issuedAt up to block.timestamp + ISSUED_AT_TOLERANCE (5 minutes) because the attester stamps with its own wall clock while block.timestamp is the proposer's. CardOracle._settle then re-checks `a.issuedAt > block.timestamp` with zero tolerance. An honest attestation signed with a clock a second ahead of the block that includes the callback reverts with BadAttestation inside the Intake's single-attempt 200k-gas try; the 0.5 IMD is spent, the request stays pending, pendingForClass blocks any new requestPrice for the class (ClassBusy) until the owner calls bindRelay and someone calls submit() in a later block, or 2 days pass for clearExpired(). Boundary: timestamp edge between attester clock and block clock. Fix: drop the strict check and rely on the base tolerance, or compare against block.timestamp + ISSUED_AT_TOLERANCE.","line":260,"path":"src/CardOracle.sol","reproduction":"Fixture: requestPrice(classId) by alice returns intakeId. Build a correctly signed attestation for that class with a.issuedAt = block.timestamp + 1 and a.expiresAt = a.issuedAt + 1 days, all other fields valid (panel 30/21/25, chainId 1, answer abi.encode(1000)). Intake delivers it with 200k gas: the callback reverts with CardOracle.BadAttestation (0x...), pendingForClass[classId] still equals intakeId, and a second requestPrice(classId) reverts ClassBusy. Expected: accepted like the base consumer accepts it (issuedAt within 5 minutes ahead). Scratch test test_issuedAtOneSecondAheadReverts passes, confirming the revert.","severity":"low","snippet":"                || a.fromBlock > a.toBlock || a.issuedAt < r.openedAt || a.issuedAt > block.timestamp","title":"_settle refuses any attestation whose issuedAt is ahead of block.timestamp by even one second, overriding the base consumer's 5-minute tolerance; the callback fails and the class stays locked"},{"citation":"resolved","description":"Interest per loan is floor(principal * aprBps * elapsed / (10_000 * 365 days)) at settlement. With aprBps = 1200 and elapsed = 30 days the numerator per unit of principal is 1200*2,592,000 = 3.1104e9 against a denominator of 3.1536e11, so any principal below 102 micro-USDC yields zero interest and every loan loses up to 1 micro-USDC of interest at each repay/extend (debt rounds down instead of up, contrary to the debt-rounds-up rule). Impact is bounded to dust per loan because extend() requires a new paid attestation each time, so it cannot be farmed profitably; reported for completeness of the precision review. Fix: round the per-loan interest up (Math.mulDiv(..., Ceil)) or enforce a minimum principal.","line":137,"path":"src/CardLending.sol","reproduction":"borrow(tokenId, 100) with aprBps 1200, warp to maturity (30 days): interestOf(id) == 0, debtOf(id) == 100, repay pulls exactly 100 (plus the 1-unit origination fee already retained). Expected: 100*0.12*30/365 = 0.986 units, rounded up to 1. Scratch test test_tinyPrincipalPaysNoInterest logs 'interest 30d on 100 units: 0'.","severity":"low","snippet":"        return _loanInterestNumerator(l) / INTEREST_DENOMINATOR;","title":"Interest truncates to zero for small principals and short extensions; borrower owes nothing on principals below ~102 units over a full 30-day term at 12% APR"},{"citation":"resolved","description":"borrow() admits amount <= 45% of price; extend() requires principal + accrued interest <= 45% of the new price, but it then pulls the interest in the same call and the surviving debt is only principal. A borrower at or near the cap with an unchanged price is told TooMuchDebt and, with repay() being full-only, must repay the whole loan and re-borrow (paying a second 1% origination fee) to continue. Boundary x invariant: the check is evaluated against a pre-payment state that the function immediately changes. Suggested fix: check principal (the post-extension debt) against the cap, or allow extend to collect interest first.","line":176,"path":"src/CardLending.sol","reproduction":"deliverPrice(1000); borrow(tokenId, 450e6) (exactly 45%); warp +1 day; deliverPrice(1000) again (same price, newer issuedAt); extend(id) by the borrower reverts TooMuchDebt because 450e6 + 147,945 interest > 450e6. Expected under the brief's 'same LTV check': the renewed loan of 450e6 principal is within 45% and extension is accepted. Scratch test test_extendAtMaxLtvUnchangedPriceFails passes, confirming the revert.","severity":"info","snippet":"        if (l.principal + interest > Math.mulDiv(p.price, MAX_LTV_BPS, BPS)) revert TooMuchDebt();","title":"extend() cannot succeed for a loan opened at the 45% cap unless the new price is higher, because the LTV check includes the interest that extend itself collects and there is no partial repayment"}],"hash":"cc1870b96c31ee37fb2c4b3632cfbb04ae0a6d008689fb2adf25f58d6084956c","nodeId":"f21e8ab1-abe7-4728-aa06-b7df916e7e1d","outcome":"completed","summary":"The review is complete and the findings file is written. Only `.imd-findings.json` and the scratch tests under `test/scratch/` were created; no source, test, or configuration file was changed.\n\n**Result: no critical or high defects in the math.** The interest accumulator, auction curve, origination fee, reserve accounting and ERC-4626 rounding all trace clean. I confirmed this with the existing 68-test suite plus scratch probes and a 256-run fuzz on the withdraw and redeem caps. Four findings are recorded, one medium and three minor.\n\n- **Medium, stale NAV during defaults.** `CardPool.totalAssets()` counts auctioned and matured debt at face value until `buy()` or `repay()` settles it. A lender who redeems during the 24-hour auction window leaves at pre-loss value and the remaining lenders absorb the whole shortfall. In the proof test, two lenders held 10,000 each, one exited after liquidation with 10,000, and the other was left holding the entire 220.5 USDC uncovered loss. A self-contained failing Foundry test is attached as the proof.\n- **Low, zero-tolerance issuedAt check.** `CardOracle._settle` rejects an attestation whose `issuedAt` is one second ahead of the block clock, though the base consumer explicitly allows five minutes of drift. The callback reverts, the fee is spent, and the class stays locked until the owner binds a relay or two days pass.\n- **Low, interest truncates to zero.** Per-loan interest rounds down, so a 30-day loan of under 102 micro-USDC owes no interest and every settlement drops up to one unit. Dust only, since each extension needs a new paid attestation.\n- **Info, extend blocked at the cap.** The extension LTV check counts the interest that the same call collects, so a loan opened at exactly 45% cannot extend at an unchanged price and must be fully repaid and reopened with a second fee.\n\n**Coverage.** All 60 listed entry points have a row, plus four invariant rows. The oracle body keys outside my area (toleranceBps, guards) were left to the oracle specialist; the recorded check response in docs shows the endpoint accepted the draft subset with wording suggestions, which I did not pursue further.","treeHash":null,"usage":{"cachedInputTokens":1511122,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":40495,"runtime":"claude","turns":33,"wallClockMs":556819}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"cdeffb0cd839cbf7","findings":[{"citation":"resolved","description":"CardPool.totalAssets() (src/CardPool.sol:52-54) is cash minus reserve plus CardLending.totalDebt(). totalDebt() adds frozenDebt, the full principal plus interest of every loan in State.Auction, at 100% from liquidate() until buy() or repay() calls pool.settle(). Nothing ever writes an auction down: a Dutch auction that reaches its 50% floor with no buyer stays in NAV at par indefinitely, and a matured loan nobody liquidates keeps accruing interest into NAV at its APR with no cap. During that window shares are priced as if the receivable were good. withdraw/redeem have no auction-aware limit, so a lender who exits between liquidate() and the settling buy() is paid at par and the realised loss (debt minus proceeds minus reserve) lands entirely on the shares left behind; a depositor who enters pays full price for shares backed by a dead receivable; a watching lender can front-run the floor-price buy() with a redeem. Merged from audit_math, audit_flow and audit_economics (same root cause, three reproductions). Minimal fix preserving the design: value an auction loan in totalDebt() at min(frozen debt, auctionPrice()) plus the reserve it can draw, so NAV follows the Dutch curve down to the floor, and add a bounded write-off for an auction that has sat at its floor so dead debt leaves NAV.","line":148,"path":"src/CardLending.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\nimport {MockCardNFT} from \"src/MockCardNFT.sol\";\nimport {CardPool} from \"src/CardPool.sol\";\nimport {CardOracle} from \"src/CardOracle.sol\";\nimport {CardLending} from \"src/CardLending.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract ProofIntake is IIntake {\n    struct Ask {\n        Callback callback;\n    }\n\n    mapping(bytes32 => Ask) private _asks;\n    uint256 public nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata callback, bytes4, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 id)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        id = keccak256(abi.encode(address(this), ++nonce));\n        _asks[id] = Ask(callback);\n    }\n\n    function deliver(bytes32 id, OracleAttestation.Attestation calldata a, bytes calldata signature) external {\n        Callback memory cb = _asks[id].callback;\n        (bool ok, bytes memory data) = cb.target.call{gas: 200_000}(abi.encodeWithSelector(cb.selector, id, a, signature));\n        if (!ok) {\n            assembly (\"memory-safe\") {\n                revert(add(data, 32), mload(data))\n            }\n        }\n    }\n}\n\n/// @notice Pool NAV counts a defaulted, unsold auction loan at face value forever. The first lender to exit\n/// takes the full pre-default value and the loss is left entirely on whoever stays.\ncontract NavBadDebtProof is Test {\n    uint256 internal constant SIGNER_KEY = 0xA11CE;\n    address internal alice = makeAddr(\"alice\");\n    address internal lenderA = makeAddr(\"lenderA\");\n    address internal lenderB = makeAddr(\"lenderB\");\n    MockUSDC internal usdc;\n    MockCardNFT internal nft;\n    CardPool internal pool;\n    CardOracle internal oracle;\n    CardLending internal lending;\n    ProofIMD internal imd;\n    ProofIntake internal intake;\n    bytes32 internal cardClass;\n    uint256 internal tokenId;\n    uint256 internal oracleNonce;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        usdc = new MockUSDC(address(this));\n        nft = new MockCardNFT(address(this));\n        pool = new CardPool(address(this), usdc);\n        imd = new ProofIMD();\n        intake = new ProofIntake();\n        oracle = new CardOracle(address(this), address(intake), address(imd), vm.addr(SIGNER_KEY));\n        lending = new CardLending(address(this), nft, pool, oracle, usdc);\n        pool.setLending(address(lending));\n        MockCardNFT.Card memory card = MockCardNFT.Card(\"Pokemon\", \"Pokemon Base Set\", \"Charizard\", \"4\", \"PSA\", \"10\", \"1\");\n        cardClass = nft.computeClassId(card);\n        tokenId = nft.mint(alice, card);\n        oracle.setClass(\n            cardClass, CardOracle.Template(\"Charizard\", \"4\", \"Pokemon Base Set\", \"pokemon-base-set\", \"charizard-4\", \"PSA 10\"), true\n        );\n        imd.mint(alice, 100 ether);\n        usdc.mint(lenderA, 10_000e6);\n        usdc.mint(lenderB, 10_000e6);\n        vm.startPrank(alice);\n        imd.approve(address(oracle), type(uint256).max);\n        nft.setApprovalForAll(address(lending), true);\n        vm.stopPrank();\n        vm.startPrank(lenderA);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lenderA);\n        vm.stopPrank();\n        vm.startPrank(lenderB);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lenderB);\n        vm.stopPrank();\n        lending.setAPR(0);\n    }\n\n    function deliverPrice(uint256 dollars) internal {\n        vm.prank(alice);\n        bytes32 id = oracle.requestPrice(cardClass);\n        OracleAttestation.Attestation memory a;\n        a.requestId = keccak256(abi.encode(\"oracle UUID\", ++oracleNonce));\n        a.chainId = 1;\n        a.questionHash = keccak256(\"canonical question document\");\n        a.answerType = 3;\n        a.answer = abi.encode(dollars);\n        a.figure = dollars;\n        a.fromBlock = 100;\n        a.toBlock = 900;\n        a.blockHash = keccak256(\"closing block\");\n        a.panelJobId = keccak256(\"panel UUID\");\n        a.panelSize = 30;\n        a.quorum = 21;\n        a.agreed = 25;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 1 days);\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, oracle.attestationDigest(a));\n        intake.deliver(id, a, abi.encodePacked(r, s, v));\n    }\n\n    function test_firstLenderExitsAtFaceValueOfUnsoldAuctionDebt() public {\n        // Alice borrows 450 USDC (45% of a $1000 card). Pool: 19,550 cash + 4.5 reserve, 450 receivable.\n        deliverPrice(1000);\n        vm.prank(alice);\n        uint256 loanId = lending.borrow(tokenId, 450e6);\n        assertEq(pool.totalAssets(), 20_000e6);\n\n        // The card's price collapses to $100: a $100 card now secures a 450 USDC debt.\n        vm.warp(block.timestamp + 1);\n        deliverPrice(100);\n        lending.liquidate(loanId);\n\n        // The auction runs its full 24 hours and reaches its permanent floor, 225 USDC, above what the card is\n        // worth. Nobody buys. The most the pool can ever recover is the floor plus the 4.5 USDC reserve.\n        vm.warp(block.timestamp + 1 days);\n        assertEq(lending.auctionPrice(loanId), 225e6);\n        uint256 cash = pool.availableLiquidity();\n        uint256 maxRecoverable = cash + lending.auctionPrice(loanId) + pool.reserve();\n        assertEq(maxRecoverable, 19_550e6 + 225e6 + 4_500_000);\n\n        // On the current code NAV still reports the dead receivable at face value (20,000e6).\n        emit log_named_uint(\"totalAssets at the unsold floor\", pool.totalAssets());\n\n        // Lender A redeems everything and must not receive more than half of what can ever be recovered.\n        uint256 sharesA = pool.balanceOf(lenderA);\n        vm.prank(lenderA);\n        uint256 received = pool.redeem(sharesA, lenderA, lenderA);\n        emit log_named_uint(\"lender A received\", received);\n        emit log_named_uint(\"fair half of recoverable\", maxRecoverable / 2);\n        assertLe(received, maxRecoverable / 2, \"exiting lender paid from the other lender's cash\");\n    }\n}","reproduction":"Two lenders deposit 10,000 USDC each (NAV 20,000e6); owner sets APR 0 for round numbers. Price 1000 delivered; alice borrows 450e6 against tokenId (fee 4.5e6 to reserve; cash 19,550e6, totalDebt 450e6, totalAssets 20,000e6). Warp +1s; a new attestation prices the class at 100; anyone calls liquidate(loanId): frozenDebt = 450e6, auction opens at 540e6. Warp +1 day: auctionPrice = 225e6, nobody buys. Maximum the pool can ever recover = 19,550e6 + 225e6 + 4.5e6 = 19,779.5e6, fair pro-rata exit 9,889.75e6 per lender. Actual: pool.totalAssets() still returns 20,000e6 and lenderA.redeem(allShares) receives 10,000,000,000 (10,000 USDC) > 9,889,750,000. Then bob calls buy(loanId, 225e6, bob): settle(225e6, 450e6) uses the 4.5e6 reserve, uncovered loss 220.5e6; lender B's previewRedeem(balance) is 9,779.5e6. Expected: each lender bears about 110.25e6. Actual: A bears 0 and B bears 220.5e6. Proof test_firstLenderExitsAtFaceValueOfUnsoldAuctionDebt fails on the current code with '10000000000 > 9889750000' (ran: forge test --match-path test/scratch/Proof_6c3d640d8ea2.t.sol).","severity":"medium","snippet":"        return activePrincipal + frozenDebt + (interestNumerator + rateWeight * (block.timestamp - lastAccrual))","title":"Pool NAV carries auctioned and matured loan debt at face value, so lenders who exit before buy() push the whole shortfall onto those who stay"},{"citation":"resolved","description":"_settle() bounds the decoded answer only from above and the body's own guard sends \"min\":\"0\" (src/CardOracle.sol:164), so an attestation whose answer is 0 passes every check and is stored as Price(0, issuedAt, expiresAt), which freshPrice() returns for 24 hours. The body's definitions tell the panel 'If unavailable, report inability, not zero', so 0 is the unavailable-column reading the contract itself says is never a price (a PriceCharting grade column showing a dash or no graded sales, read as 0 by a 21-of-30 quorum; with toleranceBps 500 all zeros agree). CardLending.liquidate() (src/CardLending.sol:194-197) then finds debt > 60% of 0 for every Active loan of the class regardless of real collateral value and opens a Dutch auction at 120% of debt, which for a 45%-LTV loan is 54% of the card's last real value; any searcher buys at the opening price and the borrower keeps only the 20%-of-debt surplus. The repository tests test_zeroNewPriceAllowsLiquidation and test_zeroIsStoredButCannotSupportBorrowing pin this in, but it contradicts the brief's rule that early liquidation needs a newer attestation 'that puts LTV above 60%': an unavailable price is not an LTV. Merged from audit_flow and audit_economics. Fix within the design: revert BadAttestation in _settle when dollars == 0 (or refuse it in freshPrice) and send \"min\":\"1\" in the body guard so the door refuses the answer before it is signed.","line":266,"path":"src/CardOracle.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {MockUSDC} from \"src/MockUSDC.sol\";\nimport {MockCardNFT} from \"src/MockCardNFT.sol\";\nimport {CardPool} from \"src/CardPool.sol\";\nimport {CardOracle} from \"src/CardOracle.sol\";\nimport {CardLending} from \"src/CardLending.sol\";\nimport {OracleAttestation} from \"src/OracleAttestation.sol\";\nimport {IIntake} from \"src/interfaces/IIntake.sol\";\n\ncontract ProofIMD2 is ERC20 {\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract ProofIntake2 is IIntake {\n    mapping(bytes32 => Callback) private _callbacks;\n    uint256 public nonce;\n\n    function priceOf(bytes32, address) external pure returns (uint256) {\n        return 0.5 ether;\n    }\n\n    function requestWithFailure(bytes32, bytes calldata, Callback calldata callback, bytes4, address asset, uint256 amount)\n        external\n        payable\n        returns (bytes32 id)\n    {\n        require(IERC20(asset).transferFrom(msg.sender, address(this), amount));\n        id = keccak256(abi.encode(address(this), ++nonce));\n        _callbacks[id] = callback;\n    }\n\n    /// @dev Mirrors the real Intake: a reverting callback is swallowed, not retried.\n    function deliver(bytes32 id, OracleAttestation.Attestation calldata a, bytes calldata signature)\n        external\n        returns (bool ok)\n    {\n        Callback memory cb = _callbacks[id];\n        (ok,) = cb.target.call{gas: 200_000}(abi.encodeWithSelector(cb.selector, id, a, signature));\n    }\n}\n\n/// @notice A signed answer of zero dollars is the oracle's \"column unavailable\" failure mode, which the body's own\n/// definitions forbid. The consumer stores it as a real price, so one such answer makes every healthy loan of the\n/// class liquidatable at a 120%-of-debt opening price.\ncontract ZeroPriceLiquidationProof is Test {\n    uint256 internal constant SIGNER_KEY = 0xA11CE;\n    address internal alice = makeAddr(\"alice\");\n    address internal bob = makeAddr(\"bob\");\n    address internal lender = makeAddr(\"lender\");\n    MockUSDC internal usdc;\n    MockCardNFT internal nft;\n    CardPool internal pool;\n    CardOracle internal oracle;\n    CardLending internal lending;\n    ProofIMD2 internal imd;\n    ProofIntake2 internal intake;\n    bytes32 internal cardClass;\n    uint256 internal tokenId;\n    uint256 internal oracleNonce;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        usdc = new MockUSDC(address(this));\n        nft = new MockCardNFT(address(this));\n        pool = new CardPool(address(this), usdc);\n        imd = new ProofIMD2();\n        intake = new ProofIntake2();\n        oracle = new CardOracle(address(this), address(intake), address(imd), vm.addr(SIGNER_KEY));\n        lending = new CardLending(address(this), nft, pool, oracle, usdc);\n        pool.setLending(address(lending));\n        MockCardNFT.Card memory card = MockCardNFT.Card(\"Pokemon\", \"Pokemon Base Set\", \"Charizard\", \"4\", \"PSA\", \"10\", \"1\");\n        cardClass = nft.computeClassId(card);\n        tokenId = nft.mint(alice, card);\n        oracle.setClass(\n            cardClass, CardOracle.Template(\"Charizard\", \"4\", \"Pokemon Base Set\", \"pokemon-base-set\", \"charizard-4\", \"PSA 10\"), true\n        );\n        imd.mint(alice, 100 ether);\n        usdc.mint(lender, 10_000e6);\n        usdc.mint(bob, 10_000e6);\n        vm.startPrank(alice);\n        imd.approve(address(oracle), type(uint256).max);\n        nft.setApprovalForAll(address(lending), true);\n        vm.stopPrank();\n        vm.startPrank(lender);\n        usdc.approve(address(pool), type(uint256).max);\n        pool.deposit(10_000e6, lender);\n        vm.stopPrank();\n        vm.prank(bob);\n        usdc.approve(address(lending), type(uint256).max);\n        lending.setAPR(0);\n    }\n\n    function deliverPrice(uint256 dollars) internal returns (bool ok) {\n        vm.prank(alice);\n        bytes32 id = oracle.requestPrice(cardClass);\n        OracleAttestation.Attestation memory a;\n        a.requestId = keccak256(abi.encode(\"oracle UUID\", ++oracleNonce));\n        a.chainId = 1;\n        a.questionHash = keccak256(\"canonical question document\");\n        a.answerType = 3;\n        a.answer = abi.encode(dollars);\n        a.figure = dollars;\n        a.fromBlock = 100;\n        a.toBlock = 900;\n        a.blockHash = keccak256(\"closing block\");\n        a.panelJobId = keccak256(\"panel UUID\");\n        a.panelSize = 30;\n        a.quorum = 21;\n        a.agreed = 25;\n        a.issuedAt = uint64(block.timestamp);\n        a.expiresAt = uint64(block.timestamp + 1 days);\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, oracle.attestationDigest(a));\n        ok = intake.deliver(id, a, abi.encodePacked(r, s, v));\n    }\n\n    function test_zeroDollarAnswerLiquidatesHealthyLoanAtOpeningPrice() public {\n        // A $1000 card secures a 450 USDC loan: 45% LTV, nowhere near the 60% liquidation line.\n        assertTrue(deliverPrice(1000));\n        vm.prank(alice);\n        uint256 loanId = lending.borrow(tokenId, 450e6);\n\n        // The panel answers 0 for the class (the \"unavailable\" reading the definitions tell it never to give).\n        vm.warp(block.timestamp + 1);\n        deliverPrice(0);\n\n        // Expected: no price means no price-triggered liquidation. Actual: 0 is a price and 450 > 60% of 0.\n        vm.expectRevert();\n        lending.liquidate(loanId);\n    }\n\n    function test_zeroDollarAnswerLetsAnyoneTakeTheCardFor540() public {\n        assertTrue(deliverPrice(1000));\n        vm.prank(alice);\n        uint256 loanId = lending.borrow(tokenId, 450e6);\n        vm.warp(block.timestamp + 1);\n        deliverPrice(0);\n        (bool liquidated,) = address(lending).call(abi.encodeWithSelector(CardLending.liquidate.selector, loanId));\n        if (liquidated) {\n            vm.prank(bob);\n            lending.buy(loanId, 540e6, bob);\n        }\n        // Expected: Alice's healthy collateral stays in custody. Actual: Bob holds the $1000 card for 540 USDC.\n        assertEq(nft.ownerOf(tokenId), address(lending), \"healthy loan collateral was auctioned on a zero answer\");\n    }\n}","reproduction":"Price 1000 delivered; alice borrows 450e6 against tokenId (45% LTV). Warp +1s; requestPrice(classId) and the panel answers abi.encode(uint256(0)) with a valid signature, panel 30/21/25: onPrice succeeds and prices[classId] = {0, now, now+1d}. Anyone calls liquidate(loanId). Expected: revert (no usable price, the loan is healthy). Actual: state -> Auction, auctionDebt 450e6, auctionPrice 540e6; bob calls buy(loanId, 540e6, bob) in the same block and owns the $1000 card for 540 USDC while alice's surplus is 90e6. Proof tests test_zeroDollarAnswerLiquidatesHealthyLoanAtOpeningPrice ('next call did not revert as expected') and test_zeroDollarAnswerLetsAnyoneTakeTheCardFor540 (NFT owned by bob, not CardLending) both fail on the current code (ran: forge test --match-path test/scratch/Proof_2c95e167fb9a.t.sol).","severity":"medium","snippet":"        if (dollars > MAX_USD) revert BadAttestation();","title":"A signed zero-dollar answer is stored as a valid fresh price and makes every healthy loan in the class liquidatable at 120% of debt"},{"citation":"resolved","description":"OracleAttestationConsumer._verifyAttestation (src/OracleAttestation.sol:161) deliberately allows issuedAt up to ISSUED_AT_TOLERANCE (5 minutes) ahead of block.timestamp because the attester stamps with its own wall clock while the block timestamp is the proposer's. _settle re-checks a.issuedAt > block.timestamp with zero tolerance. The Intake attempts the callback once under a try and does not retry, so a signed, otherwise valid attestation whose issuedAt is a second past the including block's timestamp reverts BadAttestation, the requester's 0.5 IMD is spent, nothing is stored, requests[id].pending and pendingForClass stay set, and requestPrice for the class reverts ClassBusy until the owner calls bindRelay plus someone calls submit, or REQUEST_TIMEOUT (2 days) passes for clearExpired. Borrow, extend and price-triggered liquidation for that class stall meanwhile. The README documents the stricter check as deliberate, so this is a liveness trade-off, but the failing input is realistic and the cost falls on the unprivileged requester. Merged from all four specialists. Fix: drop the strict clause and rely on the inherited tolerance (freshPrice already refuses p.issuedAt > block.timestamp at read time), or allow issuedAt up to block.timestamp + ISSUED_AT_TOLERANCE.","line":260,"path":"src/CardOracle.sol","reproduction":"Class allowlisted; alice calls requestPrice(classId) -> intakeId (0.5 IMD pulled). The Intake delivers onPrice(intakeId, a, sig) under 200,000 gas where a is correctly signed by the configured signer with a.issuedAt = block.timestamp + 1, a.expiresAt = a.issuedAt + 1 days, chainId 1, panel 30/21/25, answer abi.encode(1000). Expected: accepted, as the base library accepts an issuedAt within 5 minutes ahead. Actual: the call returns false with revert data CardOracle.BadAttestation; oracle.request(intakeId).pending stays true; a second requestPrice(classId) by bob reverts ClassBusy; clearExpired(intakeId) reverts TooEarly until block.timestamp > openedAt + 2 days. Verified in scratch test test_issuedAtOneSecondAheadRejected (passes, confirming the revert).","severity":"low","snippet":"                || a.fromBlock > a.toBlock || a.issuedAt < r.openedAt || a.issuedAt > block.timestamp","title":"_settle rejects any attestation whose issuedAt is one second ahead of block.timestamp, so an honest clock skew burns the 0.5 IMD fee and locks the class for up to two days"},{"citation":"resolved","description":"borrow() admits amount <= 45% of price. extend() pulls the accrued interest in the same call (_payPool(msg.sender, interest, interest)), so the debt that survives the extension is exactly l.principal, yet the LTV check adds the interest. The brief asks for 'extend (new attestation, same LTV check)'. A borrower who drew the advertised maximum (or anything within the accrued interest of it) is told TooMuchDebt whenever the price has not risen, and because repay() is full-only must repay the whole loan and re-borrow, paying a second 1% origination fee. Merged from audit_math and audit_flow. Fix: compare l.principal (the post-extension debt) with the cap.","line":176,"path":"src/CardLending.sol","reproduction":"deliverPrice(1000); alice borrows 450e6 (exactly 45%). Warp +1 day; deliverPrice(1000) again (same price, newer issuedAt); alice holds USDC and has approved lending. alice.extend(id): expected success (renewed debt 450e6 = 45% of 1000e6); actual revert CardLending.TooMuchDebt because 450e6 + 147,945 interest > 450e6. Verified in scratch test test_extendAtCapSamePriceReverts.","severity":"low","snippet":"        if (l.principal + interest > Math.mulDiv(p.price, MAX_LTV_BPS, BPS)) revert TooMuchDebt();","title":"extend() checks principal plus the interest it is about to collect against the 45% cap, so a loan drawn at the 45% maximum cannot be extended at an unchanged price"},{"citation":"resolved","description":"maxRedeem() ignores the dust floor redeem() enforces at src/CardPool.sol:96 (`if (shares == 0 || previewRedeem(shares) == 0) revert InvalidAmount();`). With 12-decimal shares over a 6-decimal asset, any balance below about 1e6 shares at par previews to 0 assets, so the maximum the vault advertises is exactly an amount it refuses. ERC-4626 requires maxRedeem to return the largest amount that would not revert, and routers call redeem(maxRedeem(owner)). The same divergence exists for maxWithdraw() returning 0 while withdraw(0) reverts. The stranded value is below 1e-6 USDC per holder, so impact is interface conformance only. Fix: return 0 from maxRedeem when previewRedeem(result) == 0 (and mirror in maxWithdraw), or drop the previewRedeem == 0 refusal.","line":69,"path":"src/CardPool.sol","reproduction":"Fresh pool; lender deposits 10,000e6 (10,000e12 shares) and transfers 100,000 shares to bob. pool.maxRedeem(bob) == 100000 and pool.previewRedeem(100000) == 0. bob calls pool.redeem(100000, bob, bob). Expected (ERC-4626): success. Actual: revert CardPool.InvalidAmount. Verified in scratch test test_maxRedeemDustReverts.","severity":"low","snippet":"        return Math.min(balanceOf(holder), convertToShares(availableLiquidity()));","title":"maxRedeem() returns share amounts that redeem() refuses, breaking the ERC-4626 max* guarantee for dust holders"},{"citation":"resolved","description":"The owner may move aprBps anywhere in 0..10000 with setAPR() at any time, and extend(id) has no maxAprBps parameter. A borrower who submits extend under the published 12% can be renewed at up to 100% APR for a full 30-day term if an ordinary setAPR() is mined before their transaction. The only remedy is full repayment; the loan cannot be re-extended until a strictly newer attestation exists. The owner is trusted, so this is a trust assumption with a cheap mitigation rather than a bypass. Fix that keeps the owner-set rate: add a maxAprBps argument to extend() and revert if aprBps exceeds it.","line":180,"path":"src/CardLending.sol","reproduction":"alice borrows 400e6 at aprBps 1200 against a 1000 USD price; warp 10 days; a fresh attestation is delivered. In one block: owner calls setAPR(10000); alice calls extend(id). Expected from the borrower's view: renewal at 12% or an inspectable revert. Actual: extend succeeds and lending.loan(id).aprBps == 10000; interest for the new term accrues at 100% APR. Verified in scratch test test_setAprFrontRunsExtend.","severity":"low","snippet":"        l.aprBps = aprBps;","title":"extend() takes whatever aprBps is live with no borrower-side bound, so a setAPR that lands first silently reprices the 30-day renewal"},{"citation":"resolved","description":"Interest is floor(principal * aprBps * elapsed / (10_000 * 365 days)) at settlement. At 1200 bps over 30 days the per-unit numerator is 3.1104e9 against a denominator of 3.1536e11, so any principal below 102 micro-USDC yields zero interest and every loan loses up to one micro-USDC of interest at each repay/extend. Debt should round up. Impact is dust per loan and cannot be farmed profitably because each extend needs a paid attestation. Fix: round the per-loan interest up with Math.mulDiv(..., Ceil) or enforce a minimum principal.","line":137,"path":"src/CardLending.sol","reproduction":"borrow(tokenId, 100) with aprBps 1200; warp 30 days. Expected interestOf(id) = ceil(100 * 0.12 * 30 / 365) = 1. Actual: interestOf(id) == 0 and debtOf(id) == 100; repay pulls exactly 100. Verified in scratch test test_tinyPrincipalNoInterest.","severity":"low","snippet":"        return _loanInterestNumerator(l) / INTEREST_DENOMINATOR;","title":"Per-loan interest rounds down to zero for small principals and short periods, so debt rounds in the borrower's favour"},{"citation":"resolved","description":"Guarded sets guardian = admin in the constructor and only setGuardian (onlyOwner) changes it. After transferOwnership + acceptOwnership on CardLending, CardPool and CardOracle, the former owner remains guardian on each and can pause() them (blocking borrow, extend, liquidate, deposit, mint, draw and requestPrice) while only the new owner can unpause. The README runbook tells the operator to rotate guardians explicitly, so this is recorded as a documented trust assumption, not a bypass.","line":13,"path":"src/Guarded.sol","reproduction":"Owner A calls lending.transferOwnership(B); B calls acceptOwnership() (owner() == B). A calls lending.pause(). Expected under a strict reading of 'two-step admin hand-over': A has no remaining power. Actual: pause() succeeds and paused() == true; A's unpause() reverts OwnableUnauthorizedAccount. Verified in scratch test test_oldOwnerKeepsGuardian.","severity":"info","snippet":"        guardian = admin;","title":"Trust assumption: the guardian is seeded with the deployer-admin and is not part of the two-step handover, so a former owner keeps pause power on all three guarded contracts"},{"citation":"resolved","description":"This is the brief's design and the README says a 1% reserve cannot cover a 50% outcome, so it is an economic trust assumption for the judge, not a defect. The pool's recovery depends on competitive buyers appearing within the 24-hour curve: there is no keeper reward for liquidate(), the borrower may buy their own collateral, and the opening price of 120% of debt is only 54% of collateral value for a 45%-LTV loan, so a price-triggered liquidation of an otherwise healthy loan hands a searcher a large discount while the borrower keeps 20% of debt as surplus.","line":214,"path":"src/CardLending.sol","reproduction":"alice borrows 450e6 against a $1000 card and receives 445.5e6. She never repays; at maturity (debt 454.44e6 at 12% APR) anyone calls liquidate(). No third party buys within 24 h. alice calls buy(loanId, 227.22e6, alice) at the floor: she holds the card again, net +218.28 USDC; pool.settle(227.22e6, 454.44e6) uses the 4.5e6 reserve and lenders lose 222.72e6. Repeated across loans the reserve covers about 2% of each such shortfall.","severity":"info","snippet":"        return Math.mulDiv(l.auctionDebt, numerator, BPS * AUCTION_DURATION, Math.Rounding.Ceil);","title":"Trust assumption: the Dutch auction's 120% open and 50% floor make a strategic default profitable on a thin market and the 1% reserve cannot absorb it"}],"hash":"d370a8b5e26e4efb5be9a51408611d722a8b3205fd87529db13fbde78fe02765","nodeId":"e601e665-9ae0-4d2f-a02a-6d45305dce7e","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds 9 findings and a 62-row coverage record. No source files were changed.\n\n**What I verified**\n\n- All four specialist proofs fail on the current code for the stated reasons. The project's own suite passes (125 tests, 2 Sepolia fork tests skipped), and all contracts are under the size limits.\n- The oracle body keys were a lead I checked myself: the public check endpoint refuses the full paid body in draft mode, but the IMD docs page lists `guards.sources`, `guards.minSources`, `guards.min/max`, `toleranceBps` and `consumer` as valid paid-body keys, so the body is well formed and that lead is dropped.\n- The launch manifest lists the five contracts in dependency order with one argument per constructor parameter and the brief's Sepolia addresses. No constructor calls another contract.\n\n**Findings kept, after merging duplicates**\n\n1. **Medium.** Pool NAV counts auctioned and matured debt at face value until `buy()` or `repay()` settles it, so a lender who exits during an auction is paid at par and the loss lands on those who stay. Three specialists reported this; merged with a proof.\n2. **Medium.** A signed zero-dollar answer is stored as a fresh price and lets anyone liquidate every healthy loan in the class at 120% of debt. Two specialists reported this; merged with a proof.\n3. **Low.** `_settle` rejects an `issuedAt` one second ahead of the block, burning the fee and locking the class. All four reported it; merged.\n4. **Low.** `extend()` checks principal plus the interest it collects in the same call, so a loan at the 45% cap cannot be extended at an unchanged price.\n5. **Low.** `maxRedeem()` advertises dust amounts that `redeem()` refuses.\n6. **Low.** `extend()` has no borrower-side rate bound against a same-block `setAPR`.\n7. **Low.** Per-loan interest rounds down to zero for tiny principals.\n8. **Info.** The guardian stays with the former owner after the two-step handover.\n9. **Info.** The 50% auction floor makes a strategic default profitable on a thin market.\n\nEach low finding was reproduced in a scratch test that confirmed the behaviour. The scratch directory was removed afterwards.","treeHash":null,"usage":{"cachedInputTokens":1724652,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":32913,"runtime":"claude","turns":30,"wallClockMs":481611}},{"artifacts":[],"attempt":1,"bundleHash":"21e4a96516e328a0483229cab0ccf50d052629229e2f5648edfa70d5f07d0751","device":"9fd410b500e3af05","findings":[],"hash":"d78bdae263f143f75db8aa40d43a04b2966be8653396348e4182b5e96b1a3647","nodeId":"fdadb89e-e85a-4d48-a26d-ea84e4c242e9","outcome":"completed","summary":"Created [launch.json](/home/identitymd/.identitymd/work/3d045c98-285b-45fe-8302-42d18fc8bf05/fdadb89e-e85a-4d48-a26d-ea84e4c242e9/launch.json) with five contracts in dependency order, ABI-matched arguments, and `$owner` admins.\n\nSchema checks and `forge build` passed. All 70 tests passed with the required fuzz seed.\n\nOnly `launch.json` changed outside permitted scratch files. Notes disclose the accepted implementation’s required owner binding through `CardPool.setLending`.","treeHash":"b0af2c16b3dc6cc9813748aebb3b6d06912b5893","usage":{"cachedInputTokens":299264,"inputTokens":46688,"model":"gpt-6-astra","outputTokens":4278,"runtime":"codex","turns":4,"wallClockMs":176138}},{"artifacts":[],"attempt":1,"bundleHash":"8fa6bf49077326af294a56d380963a7aaf11ed1f2b79ca755b2b2aea0736b727","device":"30a6c1a419ef4f9c","findings":[],"hash":"fd782d86695ec41077fb028ba99441585f40b941f79b5c8ff007a21175b1d3f0","nodeId":"fdadb89e-e85a-4d48-a26d-ea84e4c242e9","outcome":"completed","summary":"The existing manifest is correct, and no findings were supplied with this revision, so I left launch.json unchanged.\n\n**What I verified**\n\n- **Constructor arguments match the accepted source.** All five contracts take exactly the listed arguments in declaration order: MockUSDC and MockCardNFT take one admin, CardPool takes admin and the USDC address, CardOracle takes admin, Intake, test IMD and signer, and CardLending takes admin plus the NFT, pool, oracle and USDC addresses. Every admin is `$owner` and every cross-reference points backward.\n- **Static addresses match the brief.** The Intake and test IMD addresses are the ones the brief names. The oracle signer is an outside value the contracts check, and the live oracle service at api.imdnotdead.fun still reports that same attester today.\n- **Manifest shape is valid.** Only the three schema keys are present, all names are unique identifiers under 32 characters, every argument is a string, no contract exceeds 16 arguments, and the notes are well under the 4,000 character limit.\n- **Build and tests pass.** `forge build` succeeds and the full suite passes with the verifier's fuzz seed.\n\n| Check | Result |\n|---|---|\n| Test suites | 6 |\n| Tests passed | 80 of 80 |\n\nThe working tree is clean. No file other than launch.json was touched, and launch.json itself needed no edit.","treeHash":"e668c547a525478e0bf7faaf42639a1306e60c62","usage":{"cachedInputTokens":256931,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":3005,"runtime":"claude","turns":7,"wallClockMs":62584}}],"verification":[{"checks":[{"durationMs":7276,"exitCode":0,"name":"build","output":"Compiling 86 files with Solc 0.8.26\nSolc 0.8.26 finished in 7.18s\nCompiler run successful!\n","passed":true},{"durationMs":9088,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/SepoliaFork.t.sol:SepoliaForkTest\n[SKIP] testFork_borrowWithPreviouslyDeliveredLiveAttestation() (gas: 0)\n[SKIP] testFork_requestCharizardAgainstRealIntakeCode() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 2 skipped; finished in 405.70µs (131.41µs CPU time)\n\nRan 5 tests for test/CardAdminEdges.t.sol:CardAdminEdgesTest\n[PASS] test_constructorDependenciesCannotBeZero() (gas: 3899)\n[PASS] test_everyApplicationRejectsZeroAdmin() (gas: 4148)\n[PASS] test_guardianRotationAndRepeatedPauseBoundariesOnAllGuardedContracts() (gas: 891801)\n[PASS] test_nominationDoesNotGrantPrematureMintOrRiskAuthority() (gas: 292263)\n[PASS] test_replacedNomineeCannotAcceptOwnershipOnAnyApplication() (gas: 978773)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 3.49ms (1.12ms CPU time)\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2808998)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.19ms (2.32ms CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472652)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 286.44ms (1.63ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508712)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567939)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059907)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943483)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1506650)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948987)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503689)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285775)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725695)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485917)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601918)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1462178)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984991)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562530)\n[PASS] test_zeroIsStoredButCannotSupportBorrowing() (gas: 1531661)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 286.44ms (21.95ms CPU time)\n\nRan 7 tests for test/CardCatalogEdges.t.sol:CardCatalogEdgesTest\n[PASS] testFuzz_catalogProducesParseableBoundedJSON(bytes32,uint8) (runs: 1000, μ: 302144, ~: 278940)\nLogs:\n  Bound result 31\n\n[PASS] test_allTemplateLabelsRejectControlQuoteBackslashAndNonASCII() (gas: 973980)\n[PASS] test_eachClassFieldMattersButCertificateDoesNot() (gas: 123477)\n[PASS] test_everyMetadataFieldRejectsEmptyAndOverlongInput() (gas: 526944)\n[PASS] test_maxLengthMetadataAcceptedAndFieldsCannotAliasAcrossClassHash() (gas: 1078749)\n[PASS] test_rejectedSafeMintDoesNotConsumeCertificateOrTokenID() (gas: 690112)\n[PASS] test_slugsCannotInjectPathsQueriesOrJSON() (gas: 778630)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 286.46ms (291.61ms CPU time)\n\nRan 17 tests for test/CardOracleAdversarial.t.sol:CardOracleAdversarialTest\n[PASS] testFuzz_signedButInvalidFieldsCannotPublish(uint8) (runs: 1000, μ: 1455096, ~: 1453815)\nLogs:\n  Bound result 8\n\n[PASS] test_1271RejectionAndRevertDoNotSettleRequest() (gas: 2074992)\n[PASS] test_allFifteenAttestationFieldsAreAuthenticated() (gas: 3220629)\n[PASS] test_allOracleAdminMutationsRejectAnUnprivilegedCaller() (gas: 252093)\n[PASS] test_callbackAndRelayCanSettleAtExactRequestDeadline() (gas: 3023621)\n[PASS] test_chainDomainChangeInvalidatesAnOtherwiseValidSignature() (gas: 1553779)\n[PASS] test_feeOnTransferPaymentRejectedWithoutBurnOrPendingState() (gas: 959082)\n[PASS] test_globalUUIDReplayCannotPriceAnotherClass() (gas: 3060724)\n[PASS] test_invalidConfigurationAndMinimumPanelBoundaries() (gas: 1607105)\n[PASS] test_invalidFailureStatusIsAtomicAndFailureAllowsImmediateRetry() (gas: 2801823)\n[PASS] test_malformedAndHighSSignaturesCannotConsumePendingRequest() (gas: 1671518)\n[PASS] test_missingAllowanceAndInsufficientBalanceDoNotOpenRequests() (gas: 485948)\n[PASS] test_oldFailureOrSuccessCannotClearReplacementPendingRequest() (gas: 2889424)\n[PASS] test_relayBindingRejectsEmptyIdsConsumedUUIDAndExpiredRequest() (gas: 2890671)\n[PASS] test_sameUUIDCannotBeBoundToTwoOpenClasses() (gas: 2959985)\n[PASS] test_signerRotationImmediatelyRevokesOldSignerWithoutConsumingRequest() (gas: 1602508)\n[PASS] test_zeroAndDuplicateIntakeIDsRevertTheFeeTransfer() (gas: 2378609)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 290.89ms (245.81ms CPU time)\n\nRan 11 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 333560, ~: 336571)\nLogs:\n  Bound result 1413\n  Bound result 9890879335\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2247818)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 436939)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 202988)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2360845)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 209739)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 294616)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2556807)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 459563)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 276764)\n[PASS] test_zeroAndDustActionsRejected() (gas: 187197)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 290.79ms (205.30ms CPU time)\n\nRan 7 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719893)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81249)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210376)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 30627381)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 899314)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 314.37ms (84.72ms CPU time)\n\nRan 14 tests for test/CardLendingEdges.t.sol:CardLendingEdgesTest\n[PASS] testFuzz_auctionQuoteIsSmallestIntegerAtOrAboveLinearPrice(uint96,uint32) (runs: 1000, μ: 2040447, ~: 2040544)\nLogs:\n  Bound result 34560\n  Bound result 4019\n\n[PASS] test_NFTReceiverCannotReenterAnyLendingValuePath() (gas: 3345176)\n[PASS] test_buyInvalidReceiverMissingAllowanceAndLowBalanceAreAtomic() (gas: 2769206)\n[PASS] test_failedExtensionRestoresAPRAccrualAndMaturity() (gas: 3810521)\n[PASS] test_freshHealthyCollateralBecomesLiquidatableExactlyAtMaturity() (gas: 3461737)\n[PASS] test_maximumOracleUSDScalesAndSupportsExact45PercentBorrow() (gas: 2277309)\n[PASS] test_oddDebtAuctionRoundsUpAtStartMidpointFloorAndRemainsFrozen() (gas: 2324096)\n[PASS] test_onlyBorrowerCanExtendEvenWithNFTApproval() (gas: 3265842)\n[PASS] test_originationFeeRoundsUpAtEachCentBoundaryAndBorrowCanRepeat() (gas: 4893640)\n[PASS] test_poolPauseRollsBackBorrowIncludingNFTTransferAndLoanId() (gas: 1911124)\n[PASS] test_surplusCannotBeClaimedByBuyerOrLostOnInvalidRecipient() (gas: 2327131)\n[PASS] test_uint256MaximumBorrowFailsLTVBeforeAnyMovement() (gas: 1602536)\n[PASS] test_unconfiguredAndWrongAssetPoolCannotCreateLoan() (gas: 163112)\n[PASS] test_unknownAndClosedLoansRejectEveryLifecycleAction() (gas: 2464385)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 332.09ms (342.77ms CPU time)\n\nRan 25 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2001281, ~: 2001412)\nLogs:\n  Bound result 40642\n  Bound result 165795\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2120146, ~: 2124163)\nLogs:\n  Bound result 17546\n  Bound result 14332\n  Bound result 15\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136638)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2492698)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2026427)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2239427)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2458658)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 1989258)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750838)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2293218)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3383364)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3341093)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3573795)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1928395)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2248890)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1929862)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2174060)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5760541)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4346281)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4836281)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2133588)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2191376)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2606556)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 1977351)\n[PASS] test_zeroNewPriceAllowsLiquidation() (gas: 3334249)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 335.37ms (580.61ms CPU time)\n\nRan 8 tests for test/CardPoolEdges.t.sol:CardPoolEdgesTest\n[PASS] testFuzz_maxExitQuotesAreExecutableAfterInterestAndDonations(uint32,uint96) (runs: 1000, μ: 2226007, ~: 2226293)\nLogs:\n  Bound result 126\n  Bound result 826832661\n\n[PASS] testFuzz_repeatedDepositWithdrawAndMintRedeemCannotExtractDust(uint96,uint96) (runs: 1000, μ: 3353826, ~: 3352371)\nLogs:\n  Bound result 2299258406\n  Bound result 1470846\n  Bound result 1470846\n  Bound result 1470847\n  Bound result 1470847\n  Bound result 1470848\n  Bound result 1470848\n  Bound result 1470849\n  Bound result 1470849\n  Bound result 1470850\n  Bound result 1470850\n  Bound result 1470851\n  Bound result 1470851\n  Bound result 1470852\n  Bound result 1470852\n  Bound result 1470853\n  Bound result 1470853\n\n[PASS] test_delegatedRedeemUsesShareAllowanceAndPaysChosenReceiver() (gas: 362838)\n[PASS] test_directDebtHooksRejectInvalidAmountsWithoutChangingCashOrReserve() (gas: 172835)\n[PASS] test_failedDepositAndMintDoNotMintUnbackedShares() (gas: 233796)\n[PASS] test_failedReserveFundingCannotCreateUnbackedReserve() (gas: 124850)\n[PASS] test_unconfiguredPoolRejectsSettlementAndReserveCannotBeWithdrawn() (gas: 293352)\n[PASS] test_zeroShareDepositAfterLargeDonationRevertsWithoutTakingAssets() (gas: 209469)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 339.09ms (651.23ms CPU time)\n\nRan 2 tests for test/invariant/CardOracle.invariant.t.sol:CardOracleInvariantTest\n[PASS] invariant_requestsSettleOnceAndFeesAndPricesMatchOutcomes() (runs: 256, calls: 16384, reverts: 0)\n\n╭------------------------+-------------------+-------+---------+----------╮\n| Contract               | Selector          | Calls | Reverts | Discards |\n+=========================================================================+\n| OracleLifecycleHandler | bind              | 1544  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | catalog           | 1536  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | configure         | 1519  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | deliver           | 1487  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | donate            | 1497  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | elapse            | 1555  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | fail              | 1431  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | open              | 1461  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | rejectReplay      | 1494  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | rejectWrongSender | 1434  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | timeout           | 1426  | 0       | 0        |\n╰------------------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 195008\n  Bound result 79\n  Bound result 864000\n  Bound result 14320\n  Bound result 6\n  Bound result 73\n  Bound result 10230\n  Bound result 187695\n  Bound result 195938\n  Bound result 1800000000\n  Bound result 300000000\n  Bound result 5000000\n  Bound result 26\n  Bound result 32\n  Bound result 67\n  Bound result 228800\n  Bound result 13\n  Bound result 400000000\n  Bound result 31536000\n  Bound result 2172501663\n  Bound result 2000\n  Bound result 12013\n  Bound result 1000000000000000\n\n[PASS] test_handlerExercisesCallbacksRelaysFailureTimeoutAndRotation() (gas: 9789140)\nLogs:\n  Bound result 99\n  Bound result 1000\n  Bound result 0\n  Bound result 40\n  Bound result 1000000000000\n  Bound result 86401\n  Bound result 7000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.31s (3.31s CPU time)\n\nRan 2 tests for test/invariant/CardVault.invariant.t.sol:CardVaultInvariantTest\n[PASS] invariant_cashDebtSharesReservesSurplusAndCollateralAgree() (runs: 256, calls: 24576, reverts: 0)\n\n╭------------------+-----------+-------+---------+----------╮\n| Contract         | Selector  | Calls | Reverts | Discards |\n+===========================================================+\n| CardVaultHandler | admin     | 1906  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | borrow    | 1969  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | buy       | 1877  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | claim     | 1880  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | deposit   | 1881  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | donate    | 1934  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | elapse    | 1917  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | extend    | 1932  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | liquidate | 1855  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | repay     | 1836  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | roundTrip | 1853  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | transfer  | 1871  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | withdraw  | 1865  | 0       | 0        |\n╰------------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 10001\n  Bound result 9001\n  Bound result 9601\n  Bound result 1544000\n  Bound result 4\n  Bound result 100\n  Bound result 26992975549\n  Bound result 26992975547\n  Bound result 1\n  Bound result 693348624\n  Bound result 31\n  Bound result 3200000\n  Bound result 100000989389\n  Bound result 824576285\n  Bound result 28\n  Bound result 200000\n  Bound result 5871\n  Bound result 4147\n  Bound result 565520\n  Bound result 999999997\n  Bound result 0\n  Bound result 408889\n  Bound result 999000000\n  Bound result 2405\n  Bound result 1072000\n  Bound result 1438758\n  Bound result 27\n  Bound result 454\n  Bound result 648630\n  Bound result 186387169143\n  Bound result 1800000000\n  Bound result 100\n  Bound result 650034\n  Bound result 644458077\n  Bound result 969844860\n  Bound result 6870\n  Bound result 1000\n  Bound result 9804\n  Bound result 281333\n  Bound result 5412\n  Bound result 1\n  Bound result 4000000\n  Bound result 2486\n\n[PASS] test_handlerReachesEveryEconomicTransition() (gas: 12682062)\nLogs:\n  Bound result 20000000000\n  Bound result 10000000000\n  Bound result 10000000\n  Bound result 10000000000000\n  Bound result 10000000000000\n  Bound result 10000000\n  Bound result 5000000\n  Bound result 1000000\n  Bound result 1\n  Bound result 400000000\n  Bound result 4\n  Bound result 300000000\n  Bound result 864000\n  Bound result 1600\n  Bound result 2\n  Bound result 400000000\n  Bound result 86400\n  Bound result 0\n  Bound result 1000000\n  Bound result 1200\n  Bound result 1000000\n  Bound result 1000001\n  Bound result 1000002\n  Bound result 1000003\n  Bound result 1000004\n  Bound result 1000005\n  Bound result 1000006\n  Bound result 1000007\n  Bound result 1000008\n  Bound result 1000009\n  Bound result 1000010\n  Bound result 1000011\n  Bound result 1000012\n  Bound result 1000013\n  Bound result 1000014\n  Bound result 1000015\n  Bound result 1000016\n  Bound result 1000017\n  Bound result 1000018\n  Bound result 1000019\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 8.98s (8.96s CPU time)\n\nRan 14 test suites in 8.98s (15.05s CPU time): 125 tests passed, 0 failed, 2 skipped (127 total tests)\n","passed":true},{"durationMs":60,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":143,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":277,\"src/CardOracle.sol\":361,\"src/CardPool.sol\":125,\"src/Guarded.sol\":31,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":7,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":155,\"test/CardAdminEdges.t.sol\":120,\"test/CardCatalogEdges.t.sol\":156,\"test/CardLending.t.sol\":458,\"test/CardLendingEdges.t.sol\":355,\"test/CardOracle.t.sol\":376,\"test/CardOracleAdversarial.t.sol\":363,\"test/CardPool.t.sol\":195,\"test/CardPoolEdges.t.sol\":161,\"test/OracleConformance.t.sol\":113,\"test/README.md\":80,\"test/SepoliaFork.t.sol\":110,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/invariant/CardOracle.invariant.t.sol\":330,\"test/invariant/CardVault.invariant.t.sol\":82,\"test/invariant/CardVaultHandler.sol\":454,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"062d5a4b98e62f3a81dabf7d472e73f8a32d2b74144edf756ace7154017417ec","verifiedTreeHash":"83ab6d982b174b2dee1a28381051d8390530e1a7","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":4687,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.60s\nCompiler run successful!\n","passed":true},{"durationMs":201,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2846645)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.02ms (2.83ms CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472503)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 52.20ms (1.43ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508563)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567790)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059758)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943036)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1665706)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948850)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503551)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285786)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725546)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485768)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601901)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1461962)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984544)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562033)\n[PASS] test_zeroAnswerRefusedAndDoorRefusalClearsClass() (gas: 3532732)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 81.73ms (34.12ms CPU time)\n\nRan 12 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 415632, ~: 420837)\nLogs:\n  Bound result 5287312357\n  Bound result 5775232930\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2313727)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 549256)\n[PASS] test_maxRedeemNeverAdvertisesDustThatRedeemRefuses() (gas: 2596570)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 202989)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2426721)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 245535)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 419979)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2787793)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 461609)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 333400)\n[PASS] test_zeroAndDustActionsRejected() (gas: 205106)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 81.80ms (33.92ms CPU time)\n\nRan 9 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719804)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81272)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210712)\n[PASS] test_handoverKeepsSeparatelyAppointedGuardian() (gas: 215174)\n[PASS] test_handoverMovesGuardianSeatHeldByOutgoingOwner() (gas: 690955)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 32692633)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 919126)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 81.94ms (82.68ms CPU time)\n\nRan 32 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMarkNeverExceedsDebtOrPriceAndTracksPerLoan(uint32,uint32) (runs: 256, μ: 3123208, ~: 3143591)\nLogs:\n  Bound result 105447\n  Bound result 54\n\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2078690, ~: 2078835)\nLogs:\n  Bound result 19045\n  Bound result 54\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2156849, ~: 2160712)\nLogs:\n  Bound result 101\n  Bound result 76\n  Bound result 47\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136590)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2591315)\n[PASS] test_auctionBookAggregatesMatchPerLoanMarksWithRemovals() (gas: 5923367)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2107259)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2437401)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2608710)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 2025021)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750601)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2321310)\n[PASS] test_exitingLenderCannotLeaveUnsoldAuctionLossToOthers() (gas: 4139148)\n[PASS] test_extendAtMaximumLTVSucceedsAtUnchangedPrice() (gas: 4840909)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3385850)\n[PASS] test_extendRefusesRateAboveBorrowerLimit() (gas: 3482452)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3342540)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3612230)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1928258)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2446761)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1947693)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2293642)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5760620)\n[PASS] test_navFollowsAuctionCurveToFloorAndWriteOff() (gas: 3041166)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4445021)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4916470)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2233254)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2227053)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2710051)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 1977171)\n[PASS] test_tinyPrincipalStillOwesRoundedUpInterest() (gas: 2035757)\n[PASS] test_zeroAnswerIsNotAPriceAndCannotLiquidateHealthyLoan() (gas: 3292439)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 86.52ms (301.83ms CPU time)\n\nRan 6 test suites in 87.60ms (388.21ms CPU time): 80 tests passed, 0 failed, 0 skipped (80 total tests)\n","passed":true},{"durationMs":68,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256,uint16)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.syncAuctions()\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":144,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":417,\"src/CardOracle.sol\":364,\"src/CardPool.sol\":130,\"src/Guarded.sol\":39,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":9,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":188,\"test/CardLending.t.sol\":672,\"test/CardOracle.t.sol\":409,\"test/CardPool.t.sol\":216,\"test/OracleConformance.t.sol\":113,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4597,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[medium/high] incorrect-equality at src/CardPool.sol:72: CardPool.maxRedeem(address) (src/CardPool.sol#72-75) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/CardPool.sol:100: CardPool.redeem(uint256,address,address) (src/CardPool.sol#100-103) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/CardPool.sol:85: CardPool.deposit(uint256,address) (src/CardPool.sol#85-88) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[low/medium] reentrancy-benign at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[low/medium] timestamp at src/CardOracle.sol:301: CardOracle.freshPrice(bytes32) (src/CardOracle.sol#301-308) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:272: CardLending.buy(uint256,uint256,address) (src/CardLending.sol#272-293) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:247: CardOracle._settle(bytes32,OracleAttestation.Attestation,bytes) (src/CardOracle.sol#247-276) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:227: CardOracle.bindRelay(bytes32,bytes32,bytes32) (src/CardOracle.sol#227-239) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:348: CardLending._removeAuction(CardLending.Loan) (src/CardLending.sol#348-361) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:376: CardLending._projectBook(uint256) (src/CardLending.sol#376-405) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:292: CardOracle.clearExpired(bytes32) (src/CardOracle.sol#292-299) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:219: CardLending.extend(uint256,uint16) (src/CardLending.sol#219-239) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:174: CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:242: CardLending.liquidate(uint256) (src/CardLending.sol#242-261) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:193: CardLending.auctionMark(uint256) (src/CardLending.sol#193-199) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:320: CardLending._payPool(address,uint256,uint256) (src/CardLending.sol#320-325) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAttestation.sol:157: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/OracleAttestation.sol#157-165) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:408: CardLending._auctionMark(CardLending.Book,uint256) (src/CardLending.sol#408-416) uses timestamp for comparisons","passed":true},{"durationMs":698,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/CardLending.sol:128: Reentrancy: State change after external call (12 places)\n[low] centralization-risk at src/Admin.sol:8: Centralization Risk (17 places)\n[low] internal-function-used-once at src/OracleAttestation.sol:200: Internal Function Used Only Once\n[low] large-numeric-literal at src/CardLending.sol:48: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/CardOracle.sol:109: Literal Instead of Constant (9 places)\n[low] local-variable-shadowing at src/CardLending.sol:31: Local Variable Shadows State Variable (12 places)\n[low] non-reentrant-not-first at src/CardOracle.sol:174: `nonReentrant` is Not the First Modifier (3 places)\n[low] push-zero-opcode at src/base/USDC4626.sol:4: PUSH0 Opcode\n[low] require-revert-in-loop at src/CardOracle.sol:351: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/CardLending.sol:306: State Change Without Event\n[low] state-no-address-check at src/Guarded.sol:35: Address State Variable Set Without Checks\n[low] unchecked-return at src/MockCardNFT.sol:54: Unchecked Return (2 places)\n[low] uninitialized-local-variable at src/CardOracle.sol:351: Uninitialized Local Variable (2 places)\n[low] unspecific-solidity-pragma at src/base/USDC4626.sol:4: Unspecific Solidity Pragma","passed":true},{"durationMs":2539,"exitCode":0,"name":"proof 79849ca77b5f","output":"Compiling 69 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.90s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-5ed42f60/Proof_79849ca77b5f.t.sol:NavBadDebtProof\n[PASS] test_firstLenderExitsAtFaceValueOfUnsoldAuctionDebt() (gas: 2078415)\nLogs:\n  totalAssets at the unsold floor: 19779500000\n  lender A received: 9889750000\n  fair half of recoverable: 9889750000\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.79ms (1.75ms CPU time)\n\nRan 1 test suite in 4.57ms (3.79ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":2300,"exitCode":0,"name":"proof 3d0d0b4d38b0","output":"2026-10-15T10:10:30.147750Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-O5sa4I/repo/test/imd-proof-5ed42f60/Proof_79849ca77b5f.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 1.66s\nCompiler run successful!\n\nRan 2 tests for test/imd-proof-38f1b595/Proof_3d0d0b4d38b0.t.sol:ZeroPriceLiquidationProof\n[PASS] test_zeroDollarAnswerLetsAnyoneTakeTheCardFor540() (gas: 1556133)\n[PASS] test_zeroDollarAnswerLiquidatesHealthyLoanAtOpeningPrice() (gas: 1541297)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.96ms (2.83ms CPU time)\n\nRan 1 test suite in 3.58ms (2.96ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"0bc6e3cf400874a363027f2ef445de208effc1094e906a85b974ad6fa54736fa","verifiedTreeHash":"52b2a7ae16f50e2e25e822949d90fb287fe2a15d","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":9875,"exitCode":0,"name":"build","output":"Compiling 88 files with Solc 0.8.26\nSolc 0.8.26 finished in 9.75s\nCompiler run successful!\n","passed":true},{"durationMs":13509,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/SepoliaFork.t.sol:SepoliaForkTest\n[SKIP] testFork_borrowWithPreviouslyDeliveredLiveAttestation() (gas: 0)\n[SKIP] testFork_requestCharizardAgainstRealIntakeCode() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 2 skipped; finished in 267.55µs (179.80µs CPU time)\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2888920)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.95ms (1.76ms CPU time)\n\nRan 10 tests for test/CardRevisionEdges.t.sol:CardRevisionEdgesTest\n[PASS] test_aggregateInterestRoundsOnceWhileLoansRoundIndividually() (gas: 3125248)\n[PASS] test_auctionMarkAtEveryPhaseBoundary() (gas: 3536362)\n[PASS] test_extendAcceptsCapEqualToRateAndRefusesOnePointBelowWithoutStateChange() (gas: 5234120)\n[PASS] test_extendChecksPrincipalAgainstLTVAfterInterestIsPaid() (gas: 4878087)\n[PASS] test_futureIssuedAtWithinToleranceSettlesButCannotBorrowUntilClockCatchesUp() (gas: 3308126)\n[PASS] test_guardianAppointedDuringNominationSurvivesAcceptanceAndOldOwnerLosesPause() (gas: 708481)\n[PASS] test_laterAuctionCannotBeAdvancedPastEarlierOneByOutOfOrderClosure() (gas: 3628274)\n[PASS] test_nomineeAlreadySeatedAsGuardianKeepsSeatAndOutgoingOwnerCannotPause() (gas: 374409)\n[PASS] test_oneSecondOfInterestOnOneUnitRoundsUpToOneUnit() (gas: 2224898)\n[PASS] test_repayAndSaleAfterWriteOffLeaveNoResidualBookEntries() (gas: 3986208)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 12.26ms (10.29ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508563)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567790)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059758)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943036)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1665706)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948850)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503551)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285786)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725546)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485768)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601901)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1461962)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984544)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562033)\n[PASS] test_zeroAnswerRefusedAndDoorRefusalClearsClass() (gas: 3532732)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 40.14ms (36.90ms CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472503)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 252.43ms (1.22ms CPU time)\n\nRan 13 tests for test/CardSettlementEdges.t.sol:CardSettlementEdgesTest\n[PASS] testFuzz_navNeverRisesAndCoverNeverFallsWithTimeAlone(uint32,uint32,uint96) (runs: 1000, μ: 2653072, ~: 2658665)\nLogs:\n  Bound result 55\n  Bound result 86337\n  Bound result 182610643\n\n[PASS] testFuzz_settlementIsNavNeutralInEveryPhase(uint32,uint8,uint96) (runs: 1000, μ: 3233457, ~: 3145788)\nLogs:\n  Bound result 160170\n  Bound result 3\n  Bound result 4238\n\n[PASS] test_coverLeftWithoutAShortfallIsReleasedRatherThanHeldBack() (gas: 6310135)\n[PASS] test_directSettleIsRefusedFromAnyoneButLending() (gas: 76284)\n[PASS] test_extensionHoldsBackTheRoundingUnitTheAggregateNeverCarried() (gas: 4591663)\n[PASS] test_feeReservedAfterALossCoversOnlyLaterMarkDowns() (gas: 4788663)\n[PASS] test_heldBackReserveIsNeverLenderCashAndHasNoRelease() (gas: 3034502)\n[PASS] test_oneUnitReserveDonationCannotReRateButGrowsWithLaterMarkDowns() (gas: 2562098)\n[PASS] test_openingAnAuctionMovesNavByAtMostOneRoundingUnit() (gas: 3257612)\n[PASS] test_purchaseAtParPaysDebtSurplusAndTouchesNoReserve() (gas: 2923669)\n[PASS] test_purchaseDuringWriteDownHoldsBackTheFloorAboveTheMark() (gas: 3040259)\n[PASS] test_purchaseMidDescentSpendsExactlyTheCountedCover() (gas: 2995234)\n[PASS] test_repaymentOfADecayingLoanHoldsBackDebtAboveMarkLessSpentCover() (gas: 3014441)\nSuite result: ok. 13 passed; 0 failed; 0 skipped; finished in 552.92ms (1.05s CPU time)\n\nRan 7 tests for test/CardCatalogEdges.t.sol:CardCatalogEdgesTest\n[PASS] testFuzz_catalogProducesParseableBoundedJSON(bytes32,uint8) (runs: 1000, μ: 304617, ~: 282804)\nLogs:\n  Bound result 1\n\n[PASS] test_allTemplateLabelsRejectControlQuoteBackslashAndNonASCII() (gas: 973980)\n[PASS] test_eachClassFieldMattersButCertificateDoesNot() (gas: 123477)\n[PASS] test_everyMetadataFieldRejectsEmptyAndOverlongInput() (gas: 526944)\n[PASS] test_maxLengthMetadataAcceptedAndFieldsCannotAliasAcrossClassHash() (gas: 1078749)\n[PASS] test_rejectedSafeMintDoesNotConsumeCertificateOrTokenID() (gas: 690112)\n[PASS] test_slugsCannotInjectPathsQueriesOrJSON() (gas: 778630)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 575.04ms (577.25ms CPU time)\n\nRan 12 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 444712, ~: 449301)\nLogs:\n  Bound result 1510916412\n  Bound result 80\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2392486)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 585996)\n[PASS] test_maxRedeemNeverAdvertisesDustThatRedeemRefuses() (gas: 2690772)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 203362)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2505546)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 299827)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 465885)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2913331)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 461609)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 349886)\n[PASS] test_zeroAndDustActionsRejected() (gas: 211708)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 580.43ms (585.76ms CPU time)\n\nRan 9 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719804)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81294)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210756)\n[PASS] test_handoverKeepsSeparatelyAppointedGuardian() (gas: 215174)\n[PASS] test_handoverMovesGuardianSeatHeldByOutgoingOwner() (gas: 690978)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 33538037)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 919127)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 581.18ms (90.99ms CPU time)\n\nRan 14 tests for test/CardLendingEdges.t.sol:CardLendingEdgesTest\n[PASS] testFuzz_auctionQuoteIsSmallestIntegerAtOrAboveLinearPrice(uint96,uint32) (runs: 1000, μ: 2151942, ~: 2152036)\nLogs:\n  Bound result 11502820\n  Bound result 171241\n\n[PASS] test_NFTReceiverCannotReenterAnyLendingValuePath() (gas: 3530505)\n[PASS] test_buyInvalidReceiverMissingAllowanceAndLowBalanceAreAtomic() (gas: 2959952)\n[PASS] test_failedExtensionRestoresAPRAccrualAndMaturity() (gas: 3894089)\n[PASS] test_freshHealthyCollateralBecomesLiquidatableExactlyAtMaturity() (gas: 3573221)\n[PASS] test_maximumOracleUSDScalesAndSupportsExact45PercentBorrow() (gas: 2360990)\n[PASS] test_oddDebtAuctionRoundsUpAtStartMidpointFloorAndRemainsFrozen() (gas: 2750521)\n[PASS] test_onlyBorrowerCanExtendEvenWithNFTApproval() (gas: 3294496)\n[PASS] test_originationFeeRoundsUpAtEachCentBoundaryAndBorrowCanRepeat() (gas: 5405978)\n[PASS] test_poolPauseRollsBackBorrowIncludingNFTTransferAndLoanId() (gas: 1910866)\n[PASS] test_surplusCannotBeClaimedByBuyerOrLostOnInvalidRecipient() (gas: 2492162)\n[PASS] test_uint256MaximumBorrowFailsLTVBeforeAnyMovement() (gas: 1602457)\n[PASS] test_unconfiguredAndWrongAssetPoolCannotCreateLoan() (gas: 163068)\n[PASS] test_unknownAndClosedLoansRejectEveryLifecycleAction() (gas: 2521344)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 581.30ms (592.34ms CPU time)\n\nRan 17 tests for test/CardOracleAdversarial.t.sol:CardOracleAdversarialTest\n[PASS] testFuzz_signedButInvalidFieldsCannotPublish(uint8) (runs: 1000, μ: 1455687, ~: 1453798)\nLogs:\n  Bound result 6\n\n[PASS] test_1271RejectionAndRevertDoNotSettleRequest() (gas: 2074843)\n[PASS] test_allFifteenAttestationFieldsAreAuthenticated() (gas: 3220480)\n[PASS] test_allOracleAdminMutationsRejectAnUnprivilegedCaller() (gas: 252093)\n[PASS] test_callbackAndRelayCanSettleAtExactRequestDeadline() (gas: 3023323)\n[PASS] test_chainDomainChangeInvalidatesAnOtherwiseValidSignature() (gas: 1553630)\n[PASS] test_feeOnTransferPaymentRejectedWithoutBurnOrPendingState() (gas: 959082)\n[PASS] test_globalUUIDReplayCannotPriceAnotherClass() (gas: 3060426)\n[PASS] test_invalidConfigurationAndMinimumPanelBoundaries() (gas: 1606956)\n[PASS] test_invalidFailureStatusIsAtomicAndFailureAllowsImmediateRetry() (gas: 2801823)\n[PASS] test_malformedAndHighSSignaturesCannotConsumePendingRequest() (gas: 1671369)\n[PASS] test_missingAllowanceAndInsufficientBalanceDoNotOpenRequests() (gas: 485948)\n[PASS] test_oldFailureOrSuccessCannotClearReplacementPendingRequest() (gas: 2889275)\n[PASS] test_relayBindingRejectsEmptyIdsConsumedUUIDAndExpiredRequest() (gas: 2890522)\n[PASS] test_sameUUIDCannotBeBoundToTwoOpenClasses() (gas: 2959985)\n[PASS] test_signerRotationImmediatelyRevokesOldSignerWithoutConsumingRequest() (gas: 1602359)\n[PASS] test_zeroAndDuplicateIntakeIDsRevertTheFeeTransfer() (gas: 2378609)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 581.25ms (599.33ms CPU time)\n\nRan 8 tests for test/CardPoolEdges.t.sol:CardPoolEdgesTest\n[PASS] testFuzz_maxExitQuotesAreExecutableAfterInterestAndDonations(uint32,uint96) (runs: 1000, μ: 2393465, ~: 2393763)\nLogs:\n  Bound result 228\n  Bound result 507775177\n\n[PASS] testFuzz_repeatedDepositWithdrawAndMintRedeemCannotExtractDust(uint96,uint96) (runs: 1000, μ: 4401723, ~: 4400308)\nLogs:\n  Bound result 3910\n  Bound result 387286269\n  Bound result 569443518286138\n  Bound result 387286270\n  Bound result 569443518286139\n  Bound result 387286271\n  Bound result 569443518286140\n  Bound result 387286272\n  Bound result 569443518286141\n  Bound result 387286273\n  Bound result 569443518286142\n  Bound result 387286274\n  Bound result 569443518286143\n  Bound result 387286275\n  Bound result 569443518286144\n  Bound result 387286276\n  Bound result 569443518286145\n\n[PASS] test_delegatedRedeemUsesShareAllowanceAndPaysChosenReceiver() (gas: 460405)\n[PASS] test_directDebtHooksRejectInvalidAmountsWithoutChangingCashOrReserve() (gas: 204192)\n[PASS] test_failedDepositAndMintDoNotMintUnbackedShares() (gas: 309686)\n[PASS] test_failedReserveFundingCannotCreateUnbackedReserve() (gas: 149328)\n[PASS] test_unconfiguredPoolRejectsSettlementAndReserveCannotBeWithdrawn() (gas: 305775)\n[PASS] test_zeroShareDepositAfterLargeDonationRevertsWithoutTakingAssets() (gas: 258381)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 580.53ms (1.09s CPU time)\n\nRan 5 tests for test/CardAdminEdges.t.sol:CardAdminEdgesTest\n[PASS] test_constructorDependenciesCannotBeZero() (gas: 3899)\n[PASS] test_everyApplicationRejectsZeroAdmin() (gas: 4148)\n[PASS] test_guardianRotationAndRepeatedPauseBoundariesOnAllGuardedContracts() (gas: 892028)\n[PASS] test_nominationDoesNotGrantPrematureMintOrRiskAuthority() (gas: 292302)\n[PASS] test_replacedNomineeCannotAcceptOwnershipOnAnyApplication() (gas: 998583)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 581.44ms (1.73ms CPU time)\n\nRan 38 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMarkNeverExceedsDebtOrPriceAndTracksPerLoan(uint32,uint32) (runs: 256, μ: 3212937, ~: 3239578)\nLogs:\n  Bound result 17018\n  Bound result 1\n\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2109579, ~: 2109729)\nLogs:\n  Bound result 17018\n  Bound result 1\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2226936, ~: 2230703)\nLogs:\n  Bound result 297504893\n  Bound result 13939\n  Bound result 9\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136590)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2654484)\n[PASS] test_auctionBookAggregatesMatchPerLoanMarksWithRemovals() (gas: 6114494)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2138185)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2524140)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2685093)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 2066802)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750711)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2329542)\n[PASS] test_exitingLenderCannotLeaveUnsoldAuctionLossToOthers() (gas: 4247251)\n[PASS] test_extendAtMaximumLTVSucceedsAtUnchangedPrice() (gas: 4899363)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3444325)\n[PASS] test_extendRefusesRateAboveBorrowerLimit() (gas: 3540927)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3371249)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3683755)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1979732)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2574765)\n[PASS] test_heldBackRecoveryCoversLaterShortfallsOnly() (gas: 5437021)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1982850)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2337672)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5908726)\n[PASS] test_navFollowsAuctionCurveToFloorAndWriteOff() (gas: 3392158)\n[PASS] test_navIsContinuousThroughFloorAndWriteOff() (gas: 5754130)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4565096)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4947452)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2285074)\n[PASS] test_repayDuringDescentHoldsBackRecoveryAsReserve() (gas: 2563594)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2297023)\n[PASS] test_repayingAuctionedLoanCannotBeSandwichedForPoolRecovery() (gas: 2993530)\n[PASS] test_reserveDonationAfterLossMovesNoNAVAndCoversLaterShortfallsOnly() (gas: 2829964)\n[PASS] test_settlingWrittenOffAuctionCannotBeSandwichedForPoolRecovery() (gas: 2997201)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2773173)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 2034046)\n[PASS] test_tinyPrincipalStillOwesRoundedUpInterest() (gas: 2092677)\n[PASS] test_zeroAnswerIsNotAPriceAndCannotLiquidateHealthyLoan() (gas: 3321082)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 581.47ms (987.92ms CPU time)\n\nRan 2 tests for test/invariant/CardOracle.invariant.t.sol:CardOracleInvariantTest\n[PASS] invariant_requestsSettleOnceAndFeesAndPricesMatchOutcomes() (runs: 256, calls: 16384, reverts: 0)\n\n╭------------------------+-------------------+-------+---------+----------╮\n| Contract               | Selector          | Calls | Reverts | Discards |\n+=========================================================================+\n| OracleLifecycleHandler | bind              | 1445  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | catalog           | 1386  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | configure         | 1384  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | deliver           | 1323  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | donate            | 1407  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | elapse            | 1358  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | fail              | 1336  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | open              | 1367  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | rejectBadFigure   | 1301  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | rejectReplay      | 1345  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | rejectWrongSender | 1360  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | timeout           | 1372  | 0       | 0        |\n╰------------------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 953152080540906\n  Bound result 809751379363791175560\n  Bound result 18993\n  Bound result 259198\n  Bound result 77189\n  Bound result 54025\n  Bound result 1200\n  Bound result 1\n  Bound result 4688\n  Bound result 37\n  Bound result 141721928821518636182\n  Bound result 9300000000\n  Bound result 95\n\n[PASS] test_handlerExercisesCallbacksRelaysFailureTimeoutAndRotation() (gas: 10117104)\nLogs:\n  Bound result 99\n  Bound result 1000\n  Bound result 1\n  Bound result 40\n  Bound result 1000000000000\n  Bound result 86401\n  Bound result 7000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.91s (3.91s CPU time)\n\nRan 2 tests for test/invariant/CardVault.invariant.t.sol:CardVaultInvariantTest\n[PASS] invariant_cashDebtSharesReservesSurplusAndCollateralAgree() (runs: 256, calls: 24576, reverts: 0)\n\n╭------------------+-----------+-------+---------+----------╮\n| Contract         | Selector  | Calls | Reverts | Discards |\n+===========================================================+\n| CardVaultHandler | admin     | 1773  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | borrow    | 1759  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | buy       | 1827  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | claim     | 1726  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | deposit   | 1743  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | donate    | 1826  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | elapse    | 1755  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | extend    | 1714  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | liquidate | 1795  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | repay     | 1768  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | roundTrip | 1669  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | sandwich  | 1741  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | transfer  | 1773  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | withdraw  | 1707  | 0       | 0        |\n╰------------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 1000000\n  Bound result 10\n  Bound result 3888000\n  Bound result 8356\n  Bound result 0\n  Bound result 420048141\n  Bound result 6819\n  Bound result 5880664272\n  Bound result 2623\n  Bound result 7\n  Bound result 198780094\n  Bound result 18211797262959989\n  Bound result 9\n  Bound result 6832\n  Bound result 11908\n  Bound result 6204580\n  Bound result 6952\n  Bound result 4474\n  Bound result 436837541545\n  Bound result 0\n  Bound result 10773\n  Bound result 1135343\n  Bound result 537247501\n  Bound result 3456000\n  Bound result 4\n  Bound result 50\n  Bound result 4741\n  Bound result 1338838\n  Bound result 2660\n  Bound result 441335\n  Bound result 55239861\n  Bound result 12961\n  Bound result 1\n  Bound result 2800\n  Bound result 4469\n  Bound result 1199\n  Bound result 5896\n  Bound result 705696435776\n  Bound result 3\n  Bound result 14021\n\n[PASS] test_handlerReachesEveryEconomicTransition() (gas: 22292826)\nLogs:\n  Bound result 20000000000\n  Bound result 10000000000\n  Bound result 10000000\n  Bound result 10000000000000\n  Bound result 10000000000000\n  Bound result 10000000\n  Bound result 5000000\n  Bound result 1000000\n  Bound result 1\n  Bound result 400000000\n  Bound result 4\n  Bound result 300000000\n  Bound result 864000\n  Bound result 1600\n  Bound result 2\n  Bound result 100300001\n  Bound result 86400\n  Bound result 5\n  Bound result 400000000\n  Bound result 8\n  Bound result 300000000\n  Bound result 11\n  Bound result 200000000\n  Bound result 5000000000\n  Bound result 43200\n  Bound result 5000000000\n  Bound result 172800\n  Bound result 5000000000\n  Bound result 3\n  Bound result 100000000\n  Bound result 5000000000\n  Bound result 0\n  Bound result 1000000\n  Bound result 1200\n  Bound result 1000000\n  Bound result 1000001\n  Bound result 1000002\n  Bound result 1000003\n  Bound result 1000004\n  Bound result 1000005\n  Bound result 1000006\n  Bound result 1000007\n  Bound result 1000008\n  Bound result 1000009\n  Bound result 1000010\n  Bound result 1000011\n  Bound result 1000012\n  Bound result 1000013\n  Bound result 1000014\n  Bound result 1000015\n  Bound result 1000016\n  Bound result 1000017\n  Bound result 1000018\n  Bound result 1000019\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 13.39s (13.40s CPU time)\n\nRan 16 test suites in 13.39s (22.80s CPU time): 164 tests passed, 0 failed, 2 skipped (166 total tests)\n","passed":true},{"durationMs":108,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256,uint16)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.syncAuctions()\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256,uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":146,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":444,\"src/CardOracle.sol\":364,\"src/CardPool.sol\":180,\"src/Guarded.sol\":39,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":9,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":188,\"test/CardAdminEdges.t.sol\":120,\"test/CardCatalogEdges.t.sol\":156,\"test/CardLending.t.sol\":831,\"test/CardLendingEdges.t.sol\":364,\"test/CardOracle.t.sol\":409,\"test/CardOracleAdversarial.t.sol\":363,\"test/CardPool.t.sol\":216,\"test/CardPoolEdges.t.sol\":164,\"test/CardRevisionEdges.t.sol\":285,\"test/CardSettlementEdges.t.sol\":345,\"test/OracleConformance.t.sol\":113,\"test/README.md\":88,\"test/SepoliaFork.t.sol\":110,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/invariant/CardOracle.invariant.t.sol\":361,\"test/invariant/CardVault.invariant.t.sol\":99,\"test/invariant/CardVaultHandler.sol\":622,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"1cb70841f7e6011627ab5477f961c25439db17fa2e06d2e19527e3ecd843f8cf","verifiedTreeHash":"4309bfd6f57c0575b125d3fad9f1f1ebda68ef24","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":4673,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.56s\nCompiler run successful!\n","passed":true},{"durationMs":183,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472652)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.62ms (1.24ms CPU time)\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2808998)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.24ms (2.77ms CPU time)\n\nRan 11 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 333560, ~: 336571)\nLogs:\n  Bound result 4451552618\n  Bound result 23144502\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2247818)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 436939)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 202988)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2360845)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 209739)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 294616)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2556807)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 459563)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 276764)\n[PASS] test_zeroAndDustActionsRejected() (gas: 187197)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 50.12ms (30.74ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508712)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567939)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059907)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943483)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1506650)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948987)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503689)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285775)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725695)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485917)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601918)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1462178)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984991)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562530)\n[PASS] test_zeroIsStoredButCannotSupportBorrowing() (gas: 1531661)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 49.92ms (19.69ms CPU time)\n\nRan 7 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719893)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81249)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210376)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 30627381)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 899314)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 78.14ms (46.49ms CPU time)\n\nRan 25 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2001249, ~: 2001412)\nLogs:\n  Bound result 40642\n  Bound result 165795\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2119722, ~: 2124163)\nLogs:\n  Bound result 101\n  Bound result 76\n  Bound result 47\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136638)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2492698)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2026427)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2239427)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2458658)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 1989258)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750838)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2293218)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3383364)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3341093)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3573795)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1928395)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2248890)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1929862)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2174060)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5760541)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4346281)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4836281)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2133588)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2191376)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2606556)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 1977351)\n[PASS] test_zeroNewPriceAllowsLiquidation() (gas: 3334249)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 78.43ms (171.52ms CPU time)\n\nRan 6 test suites in 79.42ms (262.47ms CPU time): 70 tests passed, 0 failed, 0 skipped (70 total tests)\n","passed":true},{"durationMs":62,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":143,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":277,\"src/CardOracle.sol\":361,\"src/CardPool.sol\":125,\"src/Guarded.sol\":31,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":7,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":155,\"test/CardLending.t.sol\":458,\"test/CardOracle.t.sol\":376,\"test/CardPool.t.sol\":195,\"test/OracleConformance.t.sol\":113,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4486,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[medium/high] incorrect-equality at src/CardPool.sol:80: CardPool.deposit(uint256,address) (src/CardPool.sol#80-83) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/CardPool.sol:95: CardPool.redeem(uint256,address,address) (src/CardPool.sol#95-98) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[low/medium] reentrancy-benign at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[low/medium] timestamp at src/CardOracle.sol:289: CardOracle.clearExpired(bytes32) (src/CardOracle.sol#289-296) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:256: CardLending._payPool(address,uint256,uint256) (src/CardLending.sol#256-261) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:227: CardOracle.bindRelay(bytes32,bytes32,bytes32) (src/CardOracle.sol#227-239) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:298: CardOracle.freshPrice(bytes32) (src/CardOracle.sol#298-305) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:247: CardOracle._settle(bytes32,OracleAttestation.Attestation,bytes) (src/CardOracle.sol#247-273) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:174: CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:190: CardLending.liquidate(uint256) (src/CardLending.sol#190-206) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:169: CardLending.extend(uint256) (src/CardLending.sol#169-187) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAttestation.sol:157: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/OracleAttestation.sol#157-165) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:217: CardLending.buy(uint256,uint256,address) (src/CardLending.sol#217-238) uses timestamp for comparisons","passed":true},{"durationMs":699,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/CardLending.sol:98: Reentrancy: State change after external call (12 places)\n[low] centralization-risk at src/Admin.sol:8: Centralization Risk (17 places)\n[low] internal-function-used-once at src/OracleAttestation.sol:200: Internal Function Used Only Once\n[low] large-numeric-literal at src/CardLending.sol:37: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/CardOracle.sol:109: Literal Instead of Constant (9 places)\n[low] local-variable-shadowing at src/CardLending.sol:31: Local Variable Shadows State Variable (5 places)\n[low] non-reentrant-not-first at src/CardOracle.sol:174: `nonReentrant` is Not the First Modifier (3 places)\n[low] push-zero-opcode at src/base/USDC4626.sol:4: PUSH0 Opcode\n[low] require-revert-in-loop at src/CardOracle.sol:348: Loop Contains `require`/`revert` (2 places)\n[low] state-no-address-check at src/Guarded.sol:13: Address State Variable Set Without Checks\n[low] unchecked-return at src/MockCardNFT.sol:54: Unchecked Return (2 places)\n[low] uninitialized-local-variable at src/CardOracle.sol:348: Uninitialized Local Variable (2 places)\n[low] unspecific-solidity-pragma at src/base/USDC4626.sol:4: Unspecific Solidity Pragma","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"316da746d21a566836c62e1b685bb7e17588090af4afa5c4ed0d0a6d35ab7a02","verifiedTreeHash":"571af6bc22a50694d62c960fd8557a5ff4fd556d","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":4754,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.66s\nCompiler run successful!\n","passed":true},{"durationMs":167,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2888920)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.34ms (2.22ms CPU time)\n\nRan 12 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 444712, ~: 449301)\nLogs:\n  Bound result 5287312357\n  Bound result 5775232930\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2392486)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 585996)\n[PASS] test_maxRedeemNeverAdvertisesDustThatRedeemRefuses() (gas: 2690772)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 203362)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2505546)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 299827)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 465885)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2913331)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 461609)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 349886)\n[PASS] test_zeroAndDustActionsRejected() (gas: 211708)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 23.49ms (33.20ms CPU time)\n\nRan 9 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719804)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81294)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210756)\n[PASS] test_handoverKeepsSeparatelyAppointedGuardian() (gas: 215174)\n[PASS] test_handoverMovesGuardianSeatHeldByOutgoingOwner() (gas: 690978)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 33538037)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 919127)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 62.22ms (56.67ms CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472503)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 73.96ms (1.42ms CPU time)\n\nRan 38 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMarkNeverExceedsDebtOrPriceAndTracksPerLoan(uint32,uint32) (runs: 256, μ: 3208854, ~: 3224452)\nLogs:\n  Bound result 105447\n  Bound result 54\n\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2109587, ~: 2109729)\nLogs:\n  Bound result 131979\n  Bound result 171613\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2226838, ~: 2230703)\nLogs:\n  Bound result 101\n  Bound result 76\n  Bound result 47\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136590)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2654484)\n[PASS] test_auctionBookAggregatesMatchPerLoanMarksWithRemovals() (gas: 6114494)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2138185)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2524140)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2685093)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 2066802)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750711)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2329542)\n[PASS] test_exitingLenderCannotLeaveUnsoldAuctionLossToOthers() (gas: 4247251)\n[PASS] test_extendAtMaximumLTVSucceedsAtUnchangedPrice() (gas: 4899363)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3444325)\n[PASS] test_extendRefusesRateAboveBorrowerLimit() (gas: 3540927)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3371249)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3683755)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1979732)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2574765)\n[PASS] test_heldBackRecoveryCoversLaterShortfallsOnly() (gas: 5437021)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1982850)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2337672)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5908726)\n[PASS] test_navFollowsAuctionCurveToFloorAndWriteOff() (gas: 3392158)\n[PASS] test_navIsContinuousThroughFloorAndWriteOff() (gas: 5754130)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4565096)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4947452)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2285074)\n[PASS] test_repayDuringDescentHoldsBackRecoveryAsReserve() (gas: 2563594)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2297023)\n[PASS] test_repayingAuctionedLoanCannotBeSandwichedForPoolRecovery() (gas: 2993530)\n[PASS] test_reserveDonationAfterLossMovesNoNAVAndCoversLaterShortfallsOnly() (gas: 2829964)\n[PASS] test_settlingWrittenOffAuctionCannotBeSandwichedForPoolRecovery() (gas: 2997201)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2773173)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 2034046)\n[PASS] test_tinyPrincipalStillOwesRoundedUpInterest() (gas: 2092677)\n[PASS] test_zeroAnswerIsNotAPriceAndCannotLiquidateHealthyLoan() (gas: 3321082)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 73.94ms (258.40ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508563)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567790)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059758)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943036)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1665706)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948850)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503551)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285786)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725546)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485768)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601901)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1461962)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984544)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562033)\n[PASS] test_zeroAnswerRefusedAndDoorRefusalClearsClass() (gas: 3532732)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 73.99ms (34.18ms CPU time)\n\nRan 6 test suites in 75.32ms (310.93ms CPU time): 86 tests passed, 0 failed, 0 skipped (86 total tests)\n","passed":true},{"durationMs":57,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256,uint16)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.syncAuctions()\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256,uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":146,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":444,\"src/CardOracle.sol\":364,\"src/CardPool.sol\":180,\"src/Guarded.sol\":39,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":9,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":188,\"test/CardLending.t.sol\":831,\"test/CardOracle.t.sol\":409,\"test/CardPool.t.sol\":216,\"test/OracleConformance.t.sol\":113,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":4260,"exitCode":0,"name":"slither","output":"[high/medium] reentrancy-balance at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[medium/high] incorrect-equality at src/CardPool.sol:101: CardPool.maxRedeem(address) (src/CardPool.sol#101-104) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/CardPool.sol:114: CardPool.deposit(uint256,address) (src/CardPool.sol#114-117) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/CardPool.sol:129: CardPool.redeem(uint256,address,address) (src/CardPool.sol#129-132) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[medium/medium] uninitialized-local at src/CardPool.sol:82: CardPool._checkpoint().expectedShortfall (src/CardPool.sol#82) is a local variable never initialized\n[medium/medium] unused-return at src/CardPool.sol:152: CardPool.settle(uint256,uint256,uint256,uint256) (src/CardPool.sol#152-171) ignores return value by (None,shortfallAfter) = ICardDebt(lending).netReceivable() (src/CardPool.sol#158)\n[medium/medium] unused-return at src/CardPool.sol:81: CardPool._checkpoint() (src/CardPool.sol#81-86) ignores return value by (None,expectedShortfall) = ICardDebt(lending).netReceivable() (src/CardPool.sol#83)\n[medium/medium] unused-return at src/CardPool.sol:66: CardPool.coveredReserve() (src/CardPool.sol#66-70) ignores return value by (None,expectedShortfall) = ICardDebt(lending).netReceivable() (src/CardPool.sol#68)\n[low/medium] reentrancy-benign at src/CardOracle.sol:174: Reentrancy in CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213):\n[low/medium] timestamp at src/CardOracle.sol:301: CardOracle.freshPrice(bytes32) (src/CardOracle.sol#301-308) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:279: CardLending.buy(uint256,uint256,address) (src/CardLending.sol#279-299) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:247: CardOracle._settle(bytes32,OracleAttestation.Attestation,bytes) (src/CardOracle.sol#247-276) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:392: CardLending._projectBook(uint256) (src/CardLending.sol#392-425) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:227: CardOracle.bindRelay(bytes32,bytes32,bytes32) (src/CardOracle.sol#227-239) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:224: CardLending.extend(uint256,uint16) (src/CardLending.sol#224-246) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:205: CardLending.repay(uint256) (src/CardLending.sol#205-220) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:362: CardLending._removeAuction(CardLending.Loan) (src/CardLending.sol#362-376) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:249: CardLending.liquidate(uint256) (src/CardLending.sol#249-268) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:429: CardLending._auctionMark(CardLending.Book,uint256) (src/CardLending.sol#429-443) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:292: CardOracle.clearExpired(bytes32) (src/CardOracle.sol#292-299) uses timestamp for comparisons\n[low/medium] timestamp at src/CardOracle.sol:174: CardOracle.requestPrice(bytes32) (src/CardOracle.sol#174-213) uses timestamp for comparisons\n[low/medium] timestamp at src/CardLending.sol:194: CardLending.auctionMark(uint256) (src/CardLending.sol#194-202) uses timestamp for comparisons\n[low/medium] timestamp at src/OracleAttestation.sol:157: OracleAttestationConsumer._verifyAttestation(OracleAttestation.Attestation,bytes) (src/OracleAttestation.sol#157-165) uses timestamp for comparisons","passed":true},{"durationMs":613,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/CardLending.sol:130: Reentrancy: State change after external call (13 places)\n[low] centralization-risk at src/Admin.sol:8: Centralization Risk (17 places)\n[low] internal-function-used-once at src/OracleAttestation.sol:200: Internal Function Used Only Once\n[low] large-numeric-literal at src/CardLending.sol:49: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/CardOracle.sol:109: Literal Instead of Constant (9 places)\n[low] local-variable-shadowing at src/CardLending.sol:31: Local Variable Shadows State Variable (13 places)\n[low] non-reentrant-not-first at src/CardOracle.sol:174: `nonReentrant` is Not the First Modifier (3 places)\n[low] push-zero-opcode at src/base/USDC4626.sol:4: PUSH0 Opcode\n[low] require-revert-in-loop at src/CardOracle.sol:351: Loop Contains `require`/`revert` (2 places)\n[low] state-change-without-event at src/CardLending.sol:312: State Change Without Event\n[low] state-no-address-check at src/Guarded.sol:35: Address State Variable Set Without Checks\n[low] unchecked-return at src/MockCardNFT.sol:54: Unchecked Return (2 places)\n[low] uninitialized-local-variable at src/CardOracle.sol:351: Uninitialized Local Variable (2 places)\n[low] unspecific-solidity-pragma at src/base/USDC4626.sol:4: Unspecific Solidity Pragma\n[low] unused-public-function at src/CardPool.sol:66: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"37749211af0e5d9438455cd0dda2f127559a7b8db4ea2e4f220d3f2985d98451","verifiedTreeHash":"42d53dbb69b71891bde6a116c70f70fb8838d8cc","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":8111,"exitCode":0,"name":"build","output":"Compiling 87 files with Solc 0.8.26\nSolc 0.8.26 finished in 8.01s\nCompiler run successful!\n","passed":true},{"durationMs":10784,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/SepoliaFork.t.sol:SepoliaForkTest\n[SKIP] testFork_borrowWithPreviouslyDeliveredLiveAttestation() (gas: 0)\n[SKIP] testFork_requestCharizardAgainstRealIntakeCode() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 2 skipped; finished in 320.29µs (106.73µs CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472503)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.66ms (1.24ms CPU time)\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2846645)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.82ms (1.52ms CPU time)\n\nRan 5 tests for test/CardAdminEdges.t.sol:CardAdminEdgesTest\n[PASS] test_constructorDependenciesCannotBeZero() (gas: 3899)\n[PASS] test_everyApplicationRejectsZeroAdmin() (gas: 4148)\n[PASS] test_guardianRotationAndRepeatedPauseBoundariesOnAllGuardedContracts() (gas: 891962)\n[PASS] test_nominationDoesNotGrantPrematureMintOrRiskAuthority() (gas: 292302)\n[PASS] test_replacedNomineeCannotAcceptOwnershipOnAnyApplication() (gas: 998560)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 4.22ms (1.91ms CPU time)\n\nRan 9 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719804)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81272)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210712)\n[PASS] test_handoverKeepsSeparatelyAppointedGuardian() (gas: 215174)\n[PASS] test_handoverMovesGuardianSeatHeldByOutgoingOwner() (gas: 690955)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 32692633)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 919126)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 56.32ms (54.59ms CPU time)\n\nRan 12 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 415632, ~: 420837)\nLogs:\n  Bound result 4451552618\n  Bound result 23144502\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2313727)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 549256)\n[PASS] test_maxRedeemNeverAdvertisesDustThatRedeemRefuses() (gas: 2596570)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 202989)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2426721)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 245535)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 419979)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2787793)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 461609)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 333400)\n[PASS] test_zeroAndDustActionsRejected() (gas: 205106)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 166.69ms (172.03ms CPU time)\n\nRan 10 tests for test/CardRevisionEdges.t.sol:CardRevisionEdgesTest\n[PASS] test_aggregateInterestRoundsOnceWhileLoansRoundIndividually() (gas: 2908305)\n[PASS] test_auctionMarkAtEveryPhaseBoundary() (gas: 3244635)\n[PASS] test_extendAcceptsCapEqualToRateAndRefusesOnePointBelowWithoutStateChange() (gas: 5145834)\n[PASS] test_extendChecksPrincipalAgainstLTVAfterInterestIsPaid() (gas: 4819634)\n[PASS] test_futureIssuedAtWithinToleranceSettlesButCannotBorrowUntilClockCatchesUp() (gas: 3279417)\n[PASS] test_guardianAppointedDuringNominationSurvivesAcceptanceAndOldOwnerLosesPause() (gas: 708481)\n[PASS] test_laterAuctionCannotBeAdvancedPastEarlierOneByOutOfOrderClosure() (gas: 3502609)\n[PASS] test_nomineeAlreadySeatedAsGuardianKeepsSeatAndOutgoingOwnerCannotPause() (gas: 374387)\n[PASS] test_oneSecondOfInterestOnOneUnitRoundsUpToOneUnit() (gas: 2161487)\n[PASS] test_repayAndSaleAfterWriteOffLeaveNoResidualBookEntries() (gas: 3524411)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 195.92ms (17.13ms CPU time)\n\nRan 14 tests for test/CardLendingEdges.t.sol:CardLendingEdgesTest\n[PASS] testFuzz_auctionQuoteIsSmallestIntegerAtOrAboveLinearPrice(uint96,uint32) (runs: 1000, μ: 2121046, ~: 2121143)\nLogs:\n  Bound result 189848208\n  Bound result 848\n\n[PASS] test_NFTReceiverCannotReenterAnyLendingValuePath() (gas: 3438604)\n[PASS] test_buyInvalidReceiverMissingAllowanceAndLowBalanceAreAtomic() (gas: 2898573)\n[PASS] test_failedExtensionRestoresAPRAccrualAndMaturity() (gas: 3810894)\n[PASS] test_freshHealthyCollateralBecomesLiquidatableExactlyAtMaturity() (gas: 3542328)\n[PASS] test_maximumOracleUSDScalesAndSupportsExact45PercentBorrow() (gas: 2296999)\n[PASS] test_oddDebtAuctionRoundsUpAtStartMidpointFloorAndRemainsFrozen() (gas: 2623850)\n[PASS] test_onlyBorrowerCanExtendEvenWithNFTApproval() (gas: 3265831)\n[PASS] test_originationFeeRoundsUpAtEachCentBoundaryAndBorrowCanRepeat() (gas: 5018976)\n[PASS] test_poolPauseRollsBackBorrowIncludingNFTTransferAndLoanId() (gas: 1910888)\n[PASS] test_surplusCannotBeClaimedByBuyerOrLostOnInvalidRecipient() (gas: 2428805)\n[PASS] test_uint256MaximumBorrowFailsLTVBeforeAnyMovement() (gas: 1602435)\n[PASS] test_unconfiguredAndWrongAssetPoolCannotCreateLoan() (gas: 163046)\n[PASS] test_unknownAndClosedLoansRejectEveryLifecycleAction() (gas: 2464535)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 400.33ms (425.63ms CPU time)\n\nRan 17 tests for test/CardOracleAdversarial.t.sol:CardOracleAdversarialTest\n[PASS] testFuzz_signedButInvalidFieldsCannotPublish(uint8) (runs: 1000, μ: 1455819, ~: 1453798)\nLogs:\n  Bound result 6\n\n[PASS] test_1271RejectionAndRevertDoNotSettleRequest() (gas: 2074843)\n[PASS] test_allFifteenAttestationFieldsAreAuthenticated() (gas: 3220480)\n[PASS] test_allOracleAdminMutationsRejectAnUnprivilegedCaller() (gas: 252093)\n[PASS] test_callbackAndRelayCanSettleAtExactRequestDeadline() (gas: 3023323)\n[PASS] test_chainDomainChangeInvalidatesAnOtherwiseValidSignature() (gas: 1553630)\n[PASS] test_feeOnTransferPaymentRejectedWithoutBurnOrPendingState() (gas: 959082)\n[PASS] test_globalUUIDReplayCannotPriceAnotherClass() (gas: 3060426)\n[PASS] test_invalidConfigurationAndMinimumPanelBoundaries() (gas: 1606956)\n[PASS] test_invalidFailureStatusIsAtomicAndFailureAllowsImmediateRetry() (gas: 2801823)\n[PASS] test_malformedAndHighSSignaturesCannotConsumePendingRequest() (gas: 1671369)\n[PASS] test_missingAllowanceAndInsufficientBalanceDoNotOpenRequests() (gas: 485948)\n[PASS] test_oldFailureOrSuccessCannotClearReplacementPendingRequest() (gas: 2889275)\n[PASS] test_relayBindingRejectsEmptyIdsConsumedUUIDAndExpiredRequest() (gas: 2890522)\n[PASS] test_sameUUIDCannotBeBoundToTwoOpenClasses() (gas: 2959985)\n[PASS] test_signerRotationImmediatelyRevokesOldSignerWithoutConsumingRequest() (gas: 1602359)\n[PASS] test_zeroAndDuplicateIntakeIDsRevertTheFeeTransfer() (gas: 2378609)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 411.72ms (414.51ms CPU time)\n\nRan 32 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMarkNeverExceedsDebtOrPriceAndTracksPerLoan(uint32,uint32) (runs: 256, μ: 3122997, ~: 3143591)\nLogs:\n  Bound result 189811\n  Bound result 18036\n\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2078664, ~: 2078835)\nLogs:\n  Bound result 13133\n  Bound result 18036\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2156790, ~: 2160712)\nLogs:\n  Bound result 17110\n  Bound result 12873\n  Bound result 8\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136590)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2591315)\n[PASS] test_auctionBookAggregatesMatchPerLoanMarksWithRemovals() (gas: 5923367)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2107259)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2437401)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2608710)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 2025021)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750601)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2321310)\n[PASS] test_exitingLenderCannotLeaveUnsoldAuctionLossToOthers() (gas: 4139148)\n[PASS] test_extendAtMaximumLTVSucceedsAtUnchangedPrice() (gas: 4840909)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3385850)\n[PASS] test_extendRefusesRateAboveBorrowerLimit() (gas: 3482452)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3342540)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3612230)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1928258)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2446761)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1947693)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2293642)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5760620)\n[PASS] test_navFollowsAuctionCurveToFloorAndWriteOff() (gas: 3041166)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4445021)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4916470)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2233254)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2227053)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2710051)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 1977171)\n[PASS] test_tinyPrincipalStillOwesRoundedUpInterest() (gas: 2035757)\n[PASS] test_zeroAnswerIsNotAPriceAndCannotLiquidateHealthyLoan() (gas: 3292439)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 417.85ms (1.22s CPU time)\n\nRan 7 tests for test/CardCatalogEdges.t.sol:CardCatalogEdgesTest\n[PASS] testFuzz_catalogProducesParseableBoundedJSON(bytes32,uint8) (runs: 1000, μ: 302389, ~: 282804)\nLogs:\n  Bound result 1\n\n[PASS] test_allTemplateLabelsRejectControlQuoteBackslashAndNonASCII() (gas: 973980)\n[PASS] test_eachClassFieldMattersButCertificateDoesNot() (gas: 123477)\n[PASS] test_everyMetadataFieldRejectsEmptyAndOverlongInput() (gas: 526944)\n[PASS] test_maxLengthMetadataAcceptedAndFieldsCannotAliasAcrossClassHash() (gas: 1078749)\n[PASS] test_rejectedSafeMintDoesNotConsumeCertificateOrTokenID() (gas: 690112)\n[PASS] test_slugsCannotInjectPathsQueriesOrJSON() (gas: 778630)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 417.86ms (417.83ms CPU time)\n\nRan 8 tests for test/CardPoolEdges.t.sol:CardPoolEdgesTest\n[PASS] testFuzz_maxExitQuotesAreExecutableAfterInterestAndDonations(uint32,uint96) (runs: 1000, μ: 2333428, ~: 2333711)\nLogs:\n  Bound result 2497413\n  Bound result 620\n\n[PASS] testFuzz_repeatedDepositWithdrawAndMintRedeemCannotExtractDust(uint96,uint96) (runs: 1000, μ: 4146655, ~: 4145178)\nLogs:\n  Bound result 9323538530\n  Bound result 828002006\n  Bound result 14167828002006\n  Bound result 828002007\n  Bound result 14167828002007\n  Bound result 828002008\n  Bound result 14167828002008\n  Bound result 828002009\n  Bound result 14167828002009\n  Bound result 828002010\n  Bound result 14167828002010\n  Bound result 828002011\n  Bound result 14167828002011\n  Bound result 828002012\n  Bound result 14167828002012\n  Bound result 828002013\n  Bound result 14167828002013\n\n[PASS] test_delegatedRedeemUsesShareAllowanceAndPaysChosenReceiver() (gas: 437405)\n[PASS] test_directDebtHooksRejectInvalidAmountsWithoutChangingCashOrReserve() (gas: 172835)\n[PASS] test_failedDepositAndMintDoNotMintUnbackedShares() (gas: 289432)\n[PASS] test_failedReserveFundingCannotCreateUnbackedReserve() (gas: 142759)\n[PASS] test_unconfiguredPoolRejectsSettlementAndReserveCannotBeWithdrawn() (gas: 298322)\n[PASS] test_zeroShareDepositAfterLargeDonationRevertsWithoutTakingAssets() (gas: 245287)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 418.03ms (562.74ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508563)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567790)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059758)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943036)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1665706)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948850)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503551)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285786)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725546)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485768)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601901)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1461962)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984544)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562033)\n[PASS] test_zeroAnswerRefusedAndDoorRefusalClearsClass() (gas: 3532732)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 418.04ms (22.29ms CPU time)\n\nRan 2 tests for test/invariant/CardOracle.invariant.t.sol:CardOracleInvariantTest\n[PASS] invariant_requestsSettleOnceAndFeesAndPricesMatchOutcomes() (runs: 256, calls: 16384, reverts: 0)\n\n╭------------------------+-------------------+-------+---------+----------╮\n| Contract               | Selector          | Calls | Reverts | Discards |\n+=========================================================================+\n| OracleLifecycleHandler | bind              | 1445  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | catalog           | 1386  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | configure         | 1384  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | deliver           | 1323  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | donate            | 1407  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | elapse            | 1358  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | fail              | 1336  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | open              | 1367  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | rejectBadFigure   | 1301  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | rejectReplay      | 1345  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | rejectWrongSender | 1360  | 0       | 0        |\n|------------------------+-------------------+-------+---------+----------|\n| OracleLifecycleHandler | timeout           | 1372  | 0       | 0        |\n╰------------------------+-------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 953152080540906\n  Bound result 809751379363791175560\n  Bound result 8\n  Bound result 80285\n  Bound result 259198\n  Bound result 77189\n  Bound result 8163\n  Bound result 1776\n  Bound result 18808\n  Bound result 37\n  Bound result 8842710055418197234\n  Bound result 1199\n  Bound result 337461286664\n  Bound result 95\n\n[PASS] test_handlerExercisesCallbacksRelaysFailureTimeoutAndRotation() (gas: 10117104)\nLogs:\n  Bound result 99\n  Bound result 1000\n  Bound result 1\n  Bound result 40\n  Bound result 1000000000000\n  Bound result 86401\n  Bound result 7000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.25s (3.25s CPU time)\n\nRan 2 tests for test/invariant/CardVault.invariant.t.sol:CardVaultInvariantTest\n[PASS] invariant_cashDebtSharesReservesSurplusAndCollateralAgree() (runs: 256, calls: 24576, reverts: 0)\n\n╭------------------+-----------+-------+---------+----------╮\n| Contract         | Selector  | Calls | Reverts | Discards |\n+===========================================================+\n| CardVaultHandler | admin     | 1906  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | borrow    | 1969  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | buy       | 1877  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | claim     | 1880  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | deposit   | 1881  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | donate    | 1934  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | elapse    | 1917  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | extend    | 1932  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | liquidate | 1855  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | repay     | 1836  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | roundTrip | 1853  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | transfer  | 1871  | 0       | 0        |\n|------------------+-----------+-------+---------+----------|\n| CardVaultHandler | withdraw  | 1865  | 0       | 0        |\n╰------------------+-----------+-------+---------+----------╯\n\nLogs:\n  Bound result 65535\n  Bound result 9301\n  Bound result 9699\n  Bound result 1544000\n  Bound result 5\n  Bound result 1407\n  Bound result 38265120769\n  Bound result 693348624\n  Bound result 2944000\n  Bound result 100\n  Bound result 24685\n  Bound result 824576285\n  Bound result 1000000000\n  Bound result 100000000000\n  Bound result 500\n  Bound result 2346\n  Bound result 259200\n  Bound result 999999997\n  Bound result 0\n  Bound result 408889\n  Bound result 15\n  Bound result 499\n  Bound result 432000\n  Bound result 1438758\n  Bound result 2048000\n  Bound result 2169\n  Bound result 648630\n  Bound result 669986320162351\n  Bound result 900\n  Bound result 9\n  Bound result 2850308\n  Bound result 484\n  Bound result 969844860\n  Bound result 0\n  Bound result 25\n  Bound result 89\n  Bound result 659918\n  Bound result 281333\n  Bound result 5755\n  Bound result 1\n  Bound result 9804000000\n  Bound result 2481\n\n[PASS] test_handlerReachesEveryEconomicTransition() (gas: 13052134)\nLogs:\n  Bound result 20000000000\n  Bound result 10000000000\n  Bound result 10000000\n  Bound result 10000000000000\n  Bound result 10000000000000\n  Bound result 10000000\n  Bound result 5000000\n  Bound result 1000000\n  Bound result 1\n  Bound result 400000000\n  Bound result 4\n  Bound result 300000000\n  Bound result 864000\n  Bound result 1600\n  Bound result 2\n  Bound result 100300001\n  Bound result 86400\n  Bound result 0\n  Bound result 1000000\n  Bound result 1200\n  Bound result 1000000\n  Bound result 1000001\n  Bound result 1000002\n  Bound result 1000003\n  Bound result 1000004\n  Bound result 1000005\n  Bound result 1000006\n  Bound result 1000007\n  Bound result 1000008\n  Bound result 1000009\n  Bound result 1000010\n  Bound result 1000011\n  Bound result 1000012\n  Bound result 1000013\n  Bound result 1000014\n  Bound result 1000015\n  Bound result 1000016\n  Bound result 1000017\n  Bound result 1000018\n  Bound result 1000019\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 10.68s (10.68s CPU time)\n\nRan 15 test suites in 10.68s (16.84s CPU time): 145 tests passed, 0 failed, 2 skipped (147 total tests)\n","passed":true},{"durationMs":64,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256,uint16)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.syncAuctions()\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":144,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":417,\"src/CardOracle.sol\":364,\"src/CardPool.sol\":130,\"src/Guarded.sol\":39,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":9,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":188,\"test/CardAdminEdges.t.sol\":120,\"test/CardCatalogEdges.t.sol\":156,\"test/CardLending.t.sol\":672,\"test/CardLendingEdges.t.sol\":362,\"test/CardOracle.t.sol\":409,\"test/CardOracleAdversarial.t.sol\":363,\"test/CardPool.t.sol\":216,\"test/CardPoolEdges.t.sol\":161,\"test/CardRevisionEdges.t.sol\":262,\"test/OracleConformance.t.sol\":113,\"test/README.md\":85,\"test/SepoliaFork.t.sol\":110,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/invariant/CardOracle.invariant.t.sol\":361,\"test/invariant/CardVault.invariant.t.sol\":82,\"test/invariant/CardVaultHandler.sol\":509,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"3bf6144010c95f589f51762fba3db3527eb37c5d39469a94ce306a8af4f80e92","verifiedTreeHash":"95f9ace6c0d261101a90476ee47217269ed4a10f","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":4855,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.76s\nCompiler run successful!\n","passed":true},{"durationMs":205,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2888920)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.21ms (3.86ms CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472503)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 87.60ms (1.55ms CPU time)\n\nRan 12 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 444712, ~: 449301)\nLogs:\n  Bound result 7140129402\n  Bound result 1\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2392486)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 585996)\n[PASS] test_maxRedeemNeverAdvertisesDustThatRedeemRefuses() (gas: 2690772)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 203362)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2505546)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 299827)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 465885)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2913331)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 461609)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 349886)\n[PASS] test_zeroAndDustActionsRejected() (gas: 211708)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 87.65ms (46.30ms CPU time)\n\nRan 9 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719804)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81294)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210756)\n[PASS] test_handoverKeepsSeparatelyAppointedGuardian() (gas: 215174)\n[PASS] test_handoverMovesGuardianSeatHeldByOutgoingOwner() (gas: 690978)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 33538037)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 919127)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 87.70ms (55.64ms CPU time)\n\nRan 38 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMarkNeverExceedsDebtOrPriceAndTracksPerLoan(uint32,uint32) (runs: 256, μ: 3208854, ~: 3224452)\nLogs:\n  Bound result 224469\n  Bound result 2385\n\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2109587, ~: 2109729)\nLogs:\n  Bound result 17018\n  Bound result 1\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2226838, ~: 2230703)\nLogs:\n  Bound result 101\n  Bound result 76\n  Bound result 47\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136590)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2654484)\n[PASS] test_auctionBookAggregatesMatchPerLoanMarksWithRemovals() (gas: 6114494)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2138185)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2524140)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2685093)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 2066802)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750711)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2329542)\n[PASS] test_exitingLenderCannotLeaveUnsoldAuctionLossToOthers() (gas: 4247251)\n[PASS] test_extendAtMaximumLTVSucceedsAtUnchangedPrice() (gas: 4899363)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3444325)\n[PASS] test_extendRefusesRateAboveBorrowerLimit() (gas: 3540927)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3371249)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3683755)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1979732)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2574765)\n[PASS] test_heldBackRecoveryCoversLaterShortfallsOnly() (gas: 5437021)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1982850)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2337672)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5908726)\n[PASS] test_navFollowsAuctionCurveToFloorAndWriteOff() (gas: 3392158)\n[PASS] test_navIsContinuousThroughFloorAndWriteOff() (gas: 5754130)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4565096)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4947452)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2285074)\n[PASS] test_repayDuringDescentHoldsBackRecoveryAsReserve() (gas: 2563594)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2297023)\n[PASS] test_repayingAuctionedLoanCannotBeSandwichedForPoolRecovery() (gas: 2993530)\n[PASS] test_reserveDonationAfterLossMovesNoNAVAndCoversLaterShortfallsOnly() (gas: 2829964)\n[PASS] test_settlingWrittenOffAuctionCannotBeSandwichedForPoolRecovery() (gas: 2997201)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2773173)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 2034046)\n[PASS] test_tinyPrincipalStillOwesRoundedUpInterest() (gas: 2092677)\n[PASS] test_zeroAnswerIsNotAPriceAndCannotLiquidateHealthyLoan() (gas: 3321082)\nSuite result: ok. 38 passed; 0 failed; 0 skipped; finished in 87.69ms (294.80ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508563)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567790)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059758)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943036)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1665706)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948850)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503551)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285786)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725546)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485768)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601901)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1461962)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984544)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562033)\n[PASS] test_zeroAnswerRefusedAndDoorRefusalClearsClass() (gas: 3532732)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 87.74ms (36.25ms CPU time)\n\nRan 6 test suites in 88.91ms (443.59ms CPU time): 86 tests passed, 0 failed, 0 skipped (86 total tests)\n","passed":true},{"durationMs":66,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256,uint16)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.syncAuctions()\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256,uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":146,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"launch.json\":44,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":444,\"src/CardOracle.sol\":364,\"src/CardPool.sol\":180,\"src/Guarded.sol\":39,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":9,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":188,\"test/CardLending.t.sol\":831,\"test/CardOracle.t.sol\":409,\"test/CardPool.t.sol\":216,\"test/OracleConformance.t.sol\":113,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"86e6d086381200ef662ea72bcee0f5b61392b62733004e4b8d95e29858f99eda","verifiedTreeHash":"4c0a5fcee9ae3e86505619a54826239e4d8bcd9a","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":4228,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.14s\nCompiler run successful!\n","passed":true},{"durationMs":166,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2808998)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.01ms (2.90ms CPU time)\n\nRan 11 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 333560, ~: 336571)\nLogs:\n  Bound result 7140129402\n  Bound result 1\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2247818)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 436939)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 202988)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2360845)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 209739)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 294616)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2556807)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 459563)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 276764)\n[PASS] test_zeroAndDustActionsRejected() (gas: 187197)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 47.14ms (19.52ms CPU time)\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472652)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 47.14ms (1.41ms CPU time)\n\nRan 25 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2001249, ~: 2001412)\nLogs:\n  Bound result 19045\n  Bound result 54\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2119722, ~: 2124163)\nLogs:\n  Bound result 17501\n  Bound result 448\n  Bound result 0\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136638)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2492698)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2026427)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2239427)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2458658)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 1989258)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750838)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2293218)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3383364)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3341093)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3573795)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1928395)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2248890)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1929862)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2174060)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5760541)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4346281)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4836281)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2133588)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2191376)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2606556)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 1977351)\n[PASS] test_zeroNewPriceAllowsLiquidation() (gas: 3334249)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 60.99ms (141.07ms CPU time)\n\nRan 7 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719893)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81249)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210376)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 30627381)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 899314)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 69.70ms (69.20ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508712)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567939)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059907)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943483)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1506650)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948987)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503689)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285775)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725695)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485917)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601918)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1462178)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984991)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562530)\n[PASS] test_zeroIsStoredButCannotSupportBorrowing() (gas: 1531661)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 69.72ms (27.90ms CPU time)\n\nRan 6 test suites in 70.61ms (298.69ms CPU time): 70 tests passed, 0 failed, 0 skipped (70 total tests)\n","passed":true},{"durationMs":71,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":143,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"launch.json\":44,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":277,\"src/CardOracle.sol\":361,\"src/CardPool.sol\":125,\"src/Guarded.sol\":31,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":7,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":155,\"test/CardLending.t.sol\":458,\"test/CardOracle.t.sol\":376,\"test/CardPool.t.sol\":195,\"test/OracleConformance.t.sol\":113,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d78bdae263f143f75db8aa40d43a04b2966be8653396348e4182b5e96b1a3647","verifiedTreeHash":"b0af2c16b3dc6cc9813748aebb3b6d06912b5893","verifierVersion":"0.1.0+8fbf00a5"},{"checks":[{"durationMs":4971,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 4.88s\nCompiler run successful!\n","passed":true},{"durationMs":193,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 4 tests for test/OracleConformance.t.sol:OracleConformanceTest\n[PASS] test_callbackSelectorIsCanonical() (gas: 3500)\n[PASS] test_digestMatchesProtocolVectorExactly() (gas: 13581)\n[PASS] test_numericVariantAcceptsVectorSignerThroughIntake() (gas: 1472503)\n[PASS] test_protocolSignatureVerifiesWithoutReSigning() (gas: 20176)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1.62ms (1.20ms CPU time)\n\nRan 2 tests for test/SepoliaSmoke.t.sol:SepoliaSmokeTest\n[PASS] test_scriptOpensExactlyOneRequestThenBorrowsOnlyAfterCallback() (gas: 2846645)\nLogs:\n  Class ID\n  0x9add683d1b754187013fb451c05c9208c7313b9a848b22561e59fee2cafce54e\n  Token ID 1\n  Intake request ID\n  0xccd065f5146908bc71e54bea000df91e6de5aac6a72e5cd67ca6ab7ae41040ef\n  Loan ID 1\n  Gross USDC debt (six decimals) 400000000\n\n[PASS] test_scriptRefusesWrongChainBeforeAnyPayment() (gas: 53119)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.50ms (2.93ms CPU time)\n\nRan 9 tests for test/AdminAndTokens.t.sol:AdminAndTokensTest\n[PASS] test_cardMetadataImmutableClassesGroupCopiesButNotGrades() (gas: 719804)\n[PASS] test_constructorsWorkWithUndeployedDependenciesAndExplicitOwner() (gas: 81272)\n[PASS] test_guardianCanPauseButOnlyAdminUnpauses() (gas: 210712)\n[PASS] test_handoverKeepsSeparatelyAppointedGuardian() (gas: 215174)\n[PASS] test_handoverMovesGuardianSeatHeldByOutgoingOwner() (gas: 690955)\n[PASS] test_onlyOwnerMintsNFTAndMetadataIsBounded() (gas: 128131)\n[PASS] test_requestedBrandingSixDecimalsInitialSupplyAndMintAuthority() (gas: 367934)\n[PASS] test_runtimeSizeAndForbiddenOpcodesMatchProtectedFloor() (gas: 32692633)\n[PASS] test_twoStepHandoverOnEveryAdminContract() (gas: 919126)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 77.82ms (75.24ms CPU time)\n\nRan 21 tests for test/CardOracle.t.sol:CardOracleTest\n[PASS] test_callbackAfterDeadlineRejectedEvenIfFresh() (gas: 1401641)\n[PASS] test_callbackStoresSixDecimalPriceUnder200kGas() (gas: 1508563)\n[PASS] test_catalogRejectsUnknownDisabledMalformedAndBusy() (gas: 1691157)\n[PASS] test_classChangeInvalidatesPreviousPrice() (gas: 1567790)\n[PASS] test_configChangesAndPauseAreOwnerControlled() (gas: 2059758)\n[PASS] test_equalOrOlderPriceCannotOverwrite() (gas: 2943036)\n[PASS] test_expiryAndFutureTimestampRejected() (gas: 1665706)\n[PASS] test_failureOnlyClearsOwnPendingAndMovesNoValue() (gas: 2948850)\n[PASS] test_freshnessAtExpiryAndAgeBoundary() (gas: 1503551)\n[PASS] test_panelAgreementAndRequestedMinimumsEnforced() (gas: 1558558)\n[PASS] test_paymentMismatchAndNonPullingIntakeRevertAtomically() (gas: 1285786)\n[PASS] test_pendingTimeoutBoundaryAllowsRetryWithoutRefund() (gas: 2690674)\n[PASS] test_relayRequiresAdminBindingAndAnyoneCanSubmit() (gas: 1725546)\n[PASS] test_relayWrongQuestionWrongIdAndRebindingRejected() (gas: 1593243)\n[PASS] test_requestChargesCallerAndBuildsCanonicalBody() (gas: 1468925)\n[PASS] test_requestConfigSnapshotAllowsOldIntakeToDeliver() (gas: 2485768)\n[PASS] test_tamperingWrongSignerWrongDomainAndWrongChainRejected() (gas: 1601901)\n[PASS] test_timestampMustFollowOpeningAndLifetimeIsBounded() (gas: 1461962)\n[PASS] test_wrongCallerUnknownRequestAndReplayRejected() (gas: 2984544)\n[PASS] test_wrongTypeMalformedAndOversizeAnswersRejected() (gas: 1562033)\n[PASS] test_zeroAnswerRefusedAndDoorRefusalClearsClass() (gas: 3532732)\nSuite result: ok. 21 passed; 0 failed; 0 skipped; finished in 77.95ms (36.58ms CPU time)\n\nRan 12 tests for test/CardPool.t.sol:CardPoolTest\n[PASS] testFuzz_depositRedeemCannotCreateAssets(uint96,uint96) (runs: 256, μ: 415632, ~: 420837)\nLogs:\n  Bound result 5287312357\n  Bound result 5775232930\n\n[PASS] test_accruedInterestPreventsLateDepositorCapturingPastYield() (gas: 2313727)\n[PASS] test_depositMintWithdrawRedeemAndDecimals() (gas: 549256)\n[PASS] test_maxRedeemNeverAdvertisesDustThatRedeemRefuses() (gas: 2596570)\n[PASS] test_onlyConfiguredLendingCanDrawOrSettleAndCannotBeReplaced() (gas: 202989)\n[PASS] test_pauseBlocksDepositsAndNewDrawsButAllowsExitAndRepay() (gas: 2426721)\n[PASS] test_reserveFundingNeverMintsSharesOrInflatesNAV() (gas: 245535)\n[PASS] test_roundingDirectionsAfterDonation() (gas: 419979)\n[PASS] test_utilizationLimitsWithdrawalAndRedemption() (gas: 2787793)\n[PASS] test_virtualSharesPreventProfitableFirstDepositorInflation() (gas: 461609)\n[PASS] test_withdrawByApprovedShareSpender() (gas: 333400)\n[PASS] test_zeroAndDustActionsRejected() (gas: 205106)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 78.17ms (33.82ms CPU time)\n\nRan 32 tests for test/CardLending.t.sol:CardLendingTest\n[PASS] testFuzz_auctionMarkNeverExceedsDebtOrPriceAndTracksPerLoan(uint32,uint32) (runs: 256, μ: 3123208, ~: 3143591)\nLogs:\n  Bound result 127054\n  Bound result 79395\n\n[PASS] testFuzz_auctionMonotonicAndBounded(uint32,uint32) (runs: 256, μ: 2078690, ~: 2078835)\nLogs:\n  Bound result 13133\n  Bound result 18036\n\n[PASS] testFuzz_linearInterestAndRepaymentConservation(uint96,uint32,uint16) (runs: 256, μ: 2156849, ~: 2160712)\nLogs:\n  Bound result 217778531\n  Bound result 2\n  Bound result 2627\n\n[PASS] test_APRBoundsAndOwnerOnly() (gas: 136590)\n[PASS] test_NFTReceiverCannotReenterAuction() (gas: 2591315)\n[PASS] test_auctionBookAggregatesMatchPerLoanMarksWithRemovals() (gas: 5923367)\n[PASS] test_auctionExactStartMidpointEndAndBeyond() (gas: 2107259)\n[PASS] test_auctionShortfallUsesReserveThenRecognizesLenderLoss() (gas: 2437401)\n[PASS] test_auctionSurplusClaimUsesPullPayment() (gas: 2608710)\n[PASS] test_borrowCustodiesNFTPaysNetAndReservesFee() (gas: 2025021)\n[PASS] test_borrowRejectsNoPriceStalePriceZeroAndAboveLTV() (gas: 1750601)\n[PASS] test_borrowRejectsNonOwnerNoApprovalAndInsufficientLiquidity() (gas: 2321310)\n[PASS] test_exitingLenderCannotLeaveUnsoldAuctionLossToOthers() (gas: 4139148)\n[PASS] test_extendAtMaximumLTVSucceedsAtUnchangedPrice() (gas: 4840909)\n[PASS] test_extendCannotReuseSameAttestation() (gas: 3385850)\n[PASS] test_extendRefusesRateAboveBorrowerLimit() (gas: 3482452)\n[PASS] test_extendRejectsNonBorrowerExpiredLoanAndExcessiveDebt() (gas: 3342540)\n[PASS] test_extendRequiresNewPricePaysInterestAndResetsTerm() (gas: 3612230)\n[PASS] test_failedRepaymentRollsBackDebtAndCollateral() (gas: 1928258)\n[PASS] test_fundedReserveAbsorbsEntireShortfall() (gas: 2446761)\n[PASS] test_interestLinearAPRIsSnapshottedAndAccruesAfterMaturity() (gas: 1947693)\n[PASS] test_liquidationAtMaturityWithoutFreshOracle() (gas: 2293642)\n[PASS] test_multipleLoanAccrualRemovalAndExtensionConserveAssets() (gas: 5760620)\n[PASS] test_navFollowsAuctionCurveToFloorAndWriteOff() (gas: 3041166)\n[PASS] test_pauseStopsRiskButRepaymentAndAuctionSettlementRemainLive() (gas: 4445021)\n[PASS] test_priceLiquidationNeedsStrictlyNewerAttestationAndOver60Percent() (gas: 4916470)\n[PASS] test_repayDuringAuctionStopsSale() (gas: 2233254)\n[PASS] test_repayFullReturnsCollateralAndPreservesReserve() (gas: 2227053)\n[PASS] test_slippageAndRejectingReceiverRollBackSale() (gas: 2710051)\n[PASS] test_thirdPartyRepaymentReturnsCardToBorrower() (gas: 1977171)\n[PASS] test_tinyPrincipalStillOwesRoundedUpInterest() (gas: 2035757)\n[PASS] test_zeroAnswerIsNotAPriceAndCannotLiquidateHealthyLoan() (gas: 3292439)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 83.95ms (267.24ms CPU time)\n\nRan 6 test suites in 84.95ms (324.01ms CPU time): 80 tests passed, 0 failed, 0 skipped (80 total tests)\n","passed":true},{"durationMs":73,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"CardLending.acceptOwnership()\",\"CardLending.borrow(uint256,uint256)\",\"CardLending.buy(uint256,uint256,address)\",\"CardLending.claimSurplus(address)\",\"CardLending.extend(uint256,uint16)\",\"CardLending.liquidate(uint256)\",\"CardLending.pause()\",\"CardLending.repay(uint256)\",\"CardLending.setAPR(uint16)\",\"CardLending.setGuardian(address)\",\"CardLending.syncAuctions()\",\"CardLending.transferOwnership(address)\",\"CardLending.unpause()\",\"CardOracle.acceptOwnership()\",\"CardOracle.bindRelay(bytes32,bytes32,bytes32)\",\"CardOracle.clearExpired(bytes32)\",\"CardOracle.onFailure(bytes32,uint8,bytes32,uint16,uint16,bytes)\",\"CardOracle.onPrice(bytes32,(bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.pause()\",\"CardOracle.requestPrice(bytes32)\",\"CardOracle.setAction(bytes32)\",\"CardOracle.setClass(bytes32,(string,string,string,string,string,string),bool)\",\"CardOracle.setGuardian(address)\",\"CardOracle.setIntake(address)\",\"CardOracle.setObservationChain(uint256)\",\"CardOracle.setPanel(uint16,uint16)\",\"CardOracle.setPayment(address,uint256)\",\"CardOracle.setSigner(address)\",\"CardOracle.submit((bytes32,uint256,bytes32,uint8,bytes,uint256,uint64,uint64,bytes32,bytes32,uint16,uint16,uint16,uint64,uint64),bytes)\",\"CardOracle.transferOwnership(address)\",\"CardOracle.unpause()\",\"CardPool.acceptOwnership()\",\"CardPool.approve(address,uint256)\",\"CardPool.deposit(uint256,address)\",\"CardPool.draw(address,uint256,uint256)\",\"CardPool.fundReserve(uint256)\",\"CardPool.mint(uint256,address)\",\"CardPool.pause()\",\"CardPool.redeem(uint256,address,address)\",\"CardPool.setGuardian(address)\",\"CardPool.setLending(address)\",\"CardPool.settle(uint256,uint256)\",\"CardPool.transfer(address,uint256)\",\"CardPool.transferFrom(address,address,uint256)\",\"CardPool.transferOwnership(address)\",\"CardPool.unpause()\",\"CardPool.withdraw(uint256,address,address)\",\"MockCardNFT.acceptOwnership()\",\"MockCardNFT.approve(address,uint256)\",\"MockCardNFT.mint(address,(string,string,string,string,string,string,string))\",\"MockCardNFT.safeTransferFrom(address,address,uint256)\",\"MockCardNFT.safeTransferFrom(address,address,uint256,bytes)\",\"MockCardNFT.setApprovalForAll(address,bool)\",\"MockCardNFT.transferFrom(address,address,uint256)\",\"MockCardNFT.transferOwnership(address)\",\"MockUSDC.acceptOwnership()\",\"MockUSDC.approve(address,uint256)\",\"MockUSDC.mint(address,uint256)\",\"MockUSDC.transfer(address,uint256)\",\"MockUSDC.transferFrom(address,address,uint256)\",\"MockUSDC.transferOwnership(address)\"],\"files\":{\".gitignore\":7,\"LICENSE\":21,\"README.md\":144,\"THIRD_PARTY.md\":8,\"docs/charizard-body.json\":28,\"docs/oracle-check-input.json\":11,\"docs/oracle-check-response.json\":1,\"docs/oracle-check-sepolia-response.json\":1,\"foundry.toml\":18,\"launch.json\":44,\"remappings.txt\":2,\"script/SepoliaConfig.sol\":11,\"script/SepoliaSmoke.s.sol\":113,\"src/Admin.sol\":22,\"src/CardLending.sol\":417,\"src/CardOracle.sol\":364,\"src/CardPool.sol\":130,\"src/Guarded.sol\":39,\"src/MockCardNFT.sol\":66,\"src/MockUSDC.sol\":22,\"src/OracleAttestation.sol\":221,\"src/base/USDC4626.sol\":282,\"src/interfaces/ICardDebt.sol\":9,\"src/interfaces/IIntake.sol\":18,\"test/AdminAndTokens.t.sol\":188,\"test/CardLending.t.sol\":672,\"test/CardOracle.t.sol\":409,\"test/CardPool.t.sol\":216,\"test/OracleConformance.t.sol\":113,\"test/SepoliaSmoke.t.sol\":84,\"test/helpers/Fixture.sol\":126,\"test/helpers/Mocks.sol\":79,\"test/test_tools.py\":59,\"tools/check_oracle.py\":78,\"tools/watch_price.py\":42},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"fd782d86695ec41077fb028ba99441585f40b941f79b5c8ff007a21175b1d3f0","verifiedTreeHash":"e668c547a525478e0bf7faaf42639a1306e60c62","verifierVersion":"0.1.0+8fbf00a5"}]}