{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"6f250477-a6e2-4971-9187-a12f96689929","kind":"audit","nodes":[{"acceptedSubmissionHash":"4be1e2a0b39d6b926e8d67adc304349dce943ed48b8cfc974a9dd655a9a69602","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"98538215865f94325b362f7650950584629cb6397486201d98565e10ff6daa04","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"a870ffb5801849f6547868f465d73320cfda3d0d2bc353ad14ddadff16da063c","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"85a9a0961a6bd56c5acf1f29568dd572bf71ca1ce22aabd2b3155fbc02475cc7","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"7337186d87d5152589fff676295d502ff9055ca77cce7a3426e240275580f98b","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Basket (BASK) is an immutable index vault for Stock Tokens on Robinhood Chain (chain id 4663), deployed at 0xa00da50cf2b4d7446d7730a6586234319f57831c with nothing listed yet. A user deposits one listed Stock Token, priced by its Chainlink feed, and receives BASK; redeem burns BASK for a pro-rata share of every listed token. One owner and one guardian; owner changes wait 7 days and the guardian can veto. Trusted: the owner pairs each token with its true feed. The issuer can pause, block, burn or upgrade the Stock Tokens.\n\nLook hardest at:\n\n1. Redeem and claim can never be blocked or made to revert: not by the owner or guardian, a paused, blacklisted, reverting, gas-burning or lying Stock Token, a stale or wrong feed, the deposit hours, the caps or the daily limit. Check the 250,000-gas leg self-call, the 50,000-gas balance reads, the owed and totalOwed accounting, and the 64-asset gas bound.\n\n2. Nobody can move assets out of the vault except redeem and claim paying the user, and nobody can mint BASK except through deposit (plus the fee shares and the 1e15 dead shares on the first deposit). Look for any path through proposals, executeProposal, closeAsset, recognizeLoss, setFeeRecipient, finalizeGenesis or reentrancy.\n\n3. Deposit pricing and share math: rounding direction, first-deposit and donation attacks, FullMath, the 0.5% entry and exit fees, managed versus balance, and flagDeficit and recognizeLoss after an issuer burn.\n\n4. Whether the deposit gate, the 5% per-asset limit, the daily bucket or the NAV cap can be bypassed.","parentJobId":null,"planHash":"b10264ddb1cfe90fde2108e1aa4cb4ef99d0bb5c439aa1cea19098c58d480a9d","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"6f250477-a6e2-4971-9187-a12f96689929","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52129","feedbackHash":"e4ed3d3fa307059f81004f93ac88ba3559811cb326db635c0ba64824b29463b7","nodeKey":"audit_economics","submissionHash":"4be1e2a0b39d6b926e8d67adc304349dce943ed48b8cfc974a9dd655a9a69602","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52131","feedbackHash":"7c7bfbd74b86ec61039f633d8252ee2dda109d91e3a1266fb520538474674906","nodeKey":"audit_flow","submissionHash":"98538215865f94325b362f7650950584629cb6397486201d98565e10ff6daa04","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51487","feedbackHash":"aae7a7ff9533519f023909289643dffc98b34e05eb748ac0b7c6e131d75a5217","nodeKey":"audit_judge","submissionHash":"a870ffb5801849f6547868f465d73320cfda3d0d2bc353ad14ddadff16da063c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51232","feedbackHash":"2b62444557bcd8dc1bb5e10788df849339e6e86ac1930894f15ca45ad13653b4","nodeKey":"audit_math","submissionHash":"85a9a0961a6bd56c5acf1f29568dd572bf71ca1ce22aabd2b3155fbc02475cc7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51225","feedbackHash":"0d1d0b8c6dbc595f513fb525b5f4dc7b55ca9f8ddf88d4635a96fc0383e39b53","nodeKey":"audit_permissions","submissionHash":"7337186d87d5152589fff676295d502ff9055ca77cce7a3426e240275580f98b","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"84aee6e1584b26c0295df45a30b0514ba886233329fad800c510bd640970bb3a","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"63c29c49a249ab7e","findings":[{"citation":"resolved","description":"recognizeLoss is the only path that reconciles managed[token] downward to the physical balance, and deposit is the only path that raises it. There is no symmetric reconciliation when the physical balance later rises above managed. Stock Token issuers can burn or force-transfer tokens out of the vault (compliance seizure) and may later return them. If the return happens after recognizeLoss has executed, the returned tokens sit in the vault as an unmanaged surplus: NAV ignores them (deposit pricing uses managed only), redeem pays min(managed, available) = managed so no redeemer ever receives them, claim only pays owed balances, flagDeficit reverts with InvalidInput because managed <= available, and there is no rescue or sweep. The shareholders who absorbed the loss through recognizeLoss permanently lose the restored value. The 7-day recognition delay only protects against a return that happens before recognition. This is the Flow Gap seam (periphery event x first principle 'redeem returns a pro-rata share of every listed token'): the raw balance is the truth the vault custodies, but after a loss event it can never be re-adopted into accounting.","line":769,"path":"src/BaskVault.sol","reproduction":"State: 3 genesis assets priced $100, Alice deposits 10e18 of token A (managed[A]=10e18, balance 10e18). (1) Issuer burns 4e18 from the vault: balance 6e18, managed 10e18. (2) Anyone calls flagDeficit(A): deficits[A]=(4e18, now). (3) Warp 7 days; anyone calls recognizeLoss(A): managed[A]=6e18. (4) Issuer mints/returns 4e18 to the vault: balance 10e18, managed 6e18. (5) Alice redeems her entire balance (994.999e18 shares of 995e18 supply): previewRedeem/redeem pay 5.969994e18 of A, computed from managed 6e18, not from the 10e18 balance. Expected: once the issuer restores the seized tokens, holders can recover them (managed returns to 10e18, Alice's full exit pays ~9.95e18). Actual: 4e18 of A remain in the vault permanently; flagDeficit(A) reverts InvalidInput, no function can increase managed[A], and the surplus is excluded from NAV and from every redemption. Reproduced locally in test/scratch/Econ.t.sol::testSurplusAfterRecognizeLossIsStranded (logs 'stranded in vault: 4000000000000000000'). A minimal fix preserving the design: a permissionless recognizeRecovery(token) that, only for assets with a prior LossRecognized event, raises managed[token] by min(balance - totalOwed - managed, cumulative recognized loss), so donations are still ignored but restored collateral is re-adopted.","severity":"medium","snippet":"        managed[token] -= loss;","title":"Tokens returned by the issuer after recognizeLoss are stranded forever: managed can never be raised except by deposit"},{"citation":"resolved","description":"When balanceOf is unreadable (reverts, returns a wrong size, or exceeds the 50,000-gas budget after an issuer upgrade), _available substitutes managed[token] for the real balance. redeem then books the leg as owed on a managed basis and reduces managed by the same leg. If the asset is simultaneously short (issuer burn or force-transfer), the owed amount is overstated relative to what the vault physically holds. Claims are served first-come from the full balance, so the first creditor drains the asset and the remaining shareholders receive nothing from it, while a readable-balance redemption would have split the remaining balance pro rata (min(managed, available)). Owed and totalOwed stay internally consistent, but the distribution is not pro rata. Preconditions are issuer actions (unreadable balance and burn), so this is reported as low; the brief lists both powers for the issuer.","line":486,"path":"src/BaskVault.sol","reproduction":"State: Alice and Bob each deposit 10e18 of token A at $100 (managed[A]=20e18, balance 20e18, both hold ~50% of shares). (1) Issuer burns 16e18 from the vault: balance 4e18. (2) Token A's balanceOf becomes unreadable (mock balanceMode=1, i.e. reverts). (3) Alice redeems all her shares: leg = 20e18 * net / supply = 9.974927e18, booked as owed[Alice][A] (payLeg cannot read the balance so it reverts and the leg defers); managed[A] falls to 10.025e18. (4) balanceOf becomes readable again. (5) Alice calls claim(A, Alice): amount = min(9.97e18 owed, 4e18 balance) = 4e18; vault balance of A is now 0. (6) Bob redeems all his shares: leg for A = min(managed 10.025e18, available 0) = 0; Bob receives 0 and is owed 0. Expected with readable balances: each would receive ~2e18 of the remaining 4e18. Actual: Alice 4e18, Bob 0. Reproduced in test/scratch/Econ.t.sol::testUnreadableShortOverCreditsRedeemer. A minimal fix: when the balance is unreadable, defer the leg but do not reduce managed until the payment is actually made (or cap owed at the last readable available), so a later readable state can still be split pro rata.","severity":"low","snippet":"        if (!readable) return (false, managed[token]);","title":"Unreadable balance during a shortfall credits the redeemer on managed, letting the first claimant take the whole remaining balance"},{"citation":"resolved","description":"The global bucket is increased by every successful deposit and only decays linearly over 24 hours; redeem never reduces it. An actor can fill the entire bucket (max(NAV/4, $100,000)) and redeem the same block, keeping the bucket full while holding no position. All other users are then limited to the decayed slice (1/24 of the cap per hour). The attacker's cost is the round-trip fee: 0.5% exit fee plus 0.5% entry fee, and when no fee recipient is set and the attacker is the dominant holder the entry fee accrues back to the attacker, so the cost falls toward 0.5%. With the initial $100k floor this is roughly $500-$1,000 per day of deposit denial, which is cheap relative to the vault's $1M NAV cap. Redemptions already paying a fee are not credited back to the bucket, which is the asymmetry that makes the griefing free of inventory risk. This is reported as low because the documented purpose of the bucket is rate limiting and redeem/claim are unaffected.","line":611,"path":"src/BaskVault.sol","reproduction":"State: 5 genesis assets at $100, empty vault, bucket cap = max(nav2/4, 100_000e18) = 100_000e18, asset cap floor $25,000. (1) Griefer deposits 250e18 ($25,000) into each of assets 0..3 in one block: bucket = 100_000e18. (2) Alice's deposit of 1e18 ($100) into asset 4 reverts DepositUnavailable(BucketCap, asset4). (3) Griefer redeems all shares in the same block: bucket is still 100_000e18. (4) Alice's deposit still reverts with BucketCap. (5) One hour later decayedBucket() = 95_833.33e18, so at most $4,166 can be deposited by everyone combined. Griefer's total loss: 5.00001e18 tokens = ~$500 (fee recipient unset; ~$1,000 with a fee recipient set). Reproduced in test/scratch/Econ.t.sol::testBucketGriefCost. Minimal fix options that keep the rate-limit intent: subtract the redeemed value (at the redeemer's own deposit basis, or at the NAV used by the last deposit) from the bucket, or track the bucket per depositor address in addition to globally.","severity":"low","snippet":"        nextBucket = decayedBucket() + value;","title":"Daily bucket counts deposits but not redemptions, so one actor can block all deposits for a day at ~0.5%-1% of the cap"},{"citation":"resolved","description":"_depositState requires a valid, in-band, <=26h-old price and oraclePaused()==false for every asset with managed != 0, including closed assets. Chainlink's Robinhood tokenized-equity feeds have no heartbeat during off-hours and are coordinated by the issuer: a delisted or halted stock, a feed deprecated by Chainlink, a perpetual oraclePaused() flag, or an asset that has moved outside its static 4x band without an owner band proposal makes _priceOK false for a managed asset, and then no deposit of any token is possible. The owner's tools do not help: closeAsset only blocks deposits of that token; proposeBand reverts because the band answer must be strictly fresher than 26 hours; proposeFeed must point at a readable in-band feed, which for a dead stock means pairing the token with a feed that is not its true feed, violating the stated trust assumption. The only accounting path that removes the asset from pricing is recognizeLoss after an issuer burn, which the vault cannot cause. Redeem and claim are unaffected, which is why this is low, but a 64-asset basket has a materially higher probability of at least one permanently unpriceable constituent over its life, and the vault has no delist/mark-to-zero/exclude-from-NAV mechanism to recover deposit liveness.","line":558,"path":"src/BaskVault.sol","reproduction":"State: 4 genesis assets at $100, Alice deposits 1e18 of asset 0 and 1e18 of asset 1 (both managed). (1) Asset 0's feed stops updating (updatedAt frozen). (2) Guardian calls closeAsset(asset0). (3) Seven days later feeds 1..3 are fresh. depositStatus(asset1) returns (InvalidPrice, asset0) and deposit(asset1, ...) reverts DepositUnavailable(10, asset0); the same holds for every other token. (4) Owner calls proposeBand(asset0); after 7 days executeProposal reverts InvalidFeed(feed0) because the answer is >= 26 hours old. (5) proposeFeed(asset0, feedX) requires feedX to be readable and inside asset0's current band; no honest feed for a dead stock satisfies this. Expected: an owner/guardian action (with the 7-day proposal delay and guardian veto) can retire or zero-weight an unpriceable constituent so deposits resume. Actual: deposits remain blocked until the issuer burns the vault's tokens or the owner pairs the token with a non-true feed. Reproduced in test/scratch/Econ.t.sol::testStaleFeedOnManagedAssetBlocksAllDepositsPermanently (reason 10 with fault = asset 0). A minimal fix preserving the design: a timelocked, guardian-vetoable 'Retire' proposal kind that marks the asset so it is skipped in the NAV/price loop (its balance stays redeemable pro rata via the min(managed, available) leg) and is excluded from the market-freshness count.","severity":"low","snippet":"                if (!_priceOK(a, reads[i], answers[i], times[i])) return (Reason.InvalidPrice, a.token, 0, 0);","title":"A managed asset whose feed stops or stays paused freezes every deposit indefinitely; closeAsset does not exclude it and no retirement path exists"}],"hash":"4be1e2a0b39d6b926e8d67adc304349dce943ed48b8cfc974a9dd655a9a69602","nodeId":"2b02d3a8-1018-429b-acea-b74736f41598","outcome":"completed","summary":"Nothing further is needed: the findings file parses, all four snippets match their source lines exactly, the full suite passes with the scratch file included (67 tests), and no file outside the allowed scratch area and the findings file was changed. Here is the recap.\n\n**Outcome.** The vault is tightly built. I found no critical or high defect, so no finding carries a proof. Four substantiated defects are recorded in `.imd-findings.json`, one medium and three low, each with a concrete failing state and a numeric reproduction in `test/scratch/Econ.t.sol`.\n\n**Findings**\n- **Medium, stranded restitution.** After an issuer burn is recognized through `recognizeLoss`, tokens the issuer later returns can never re-enter accounting. NAV ignores them, redeem pays only managed amounts, and `flagDeficit` reverts. In the reproduction 4e18 of a 10e18 position is locked forever.\n- **Low, unreadable plus short asset.** When `balanceOf` is unreadable, the vault books redemption legs on managed rather than the real balance. If the asset is also short, the first claimant takes the whole remaining balance. Alice took all 4e18; Bob received nothing.\n- **Low, daily bucket griefing.** Redemptions never reduce the bucket, so filling it and exiting in the same block blocks every other deposit for a day at a cost near $500 to $1,000.\n- **Low, no retirement path.** One managed asset with a dead feed, a perpetual oracle pause, or an out-of-band price blocks all deposits. Closing the asset does not help, band re-centering reverts on stale answers, and the only feed-replacement route violates the true-feed trust assumption.\n\n**What held up under review.** Redeem and claim cannot be blocked by roles, pauses, caps, feeds, or hostile tokens. The 250k leg self-call, 50k balance reads, owed and totalOwed accounting, and the 64-asset loop are sound. The worst case measured 22.7M gas against Arbitrum Nitro's 32M per-transaction cap. No path mints BASK or moves assets outside deposit, redeem and claim. Share math rounds in the vault's favor, donations are ignored, and the 1e15 dead-share floor makes first-deposit inflation infeasible. The deposit gate, per-asset cap, bucket and NAV cap cannot be bypassed, only griefed as noted.\n\n**Live verification on chain 4663.** The vault is deployed at the stated address with the expected owner and no assets. The hard-coded factory exists and its implementation carries the `tokenAddress(bytes32)` selector. Real Stock Tokens expose `uid()`, `oraclePaused()` and 18 decimals, and the factory round-trips their uid. The Chainlink feeds expose `aggregator()` with 8 decimals, a 24-hour heartbeat and a 0.5% deviation threshold. Corporate actions such as splits are handled by the feed's multiplier without changing raw balances, which rules out a split-surplus failure I had initially traced. A real token `transfer` into the vault costs about 55k gas and `balanceOf` about 11.6k, both well inside the vault's budgets. Five exchange-rate feeds use 18 decimals and cannot be listed, which is a limitation rather than a defect.\n\n**Trust assumptions to document.** The owner pairs each token with its true feed and can instantly pause deposits, close assets, lower the NAV cap to zero, and set the permanent fee recipient once. The guardian can pause and close instantly and veto all proposals except its own replacement. Issuers can burn, pause, block or upgrade tokens, which can defer or prevent delivery but not the accounting of redemptions. Corporate cash events such as acquisitions have no on-chain destination for cash, so that value is lost to holders.\n\nSources: [Robinhood Chain RPC](https://www.dwellir.com/docs/robinhood), [Chainlink Robinhood tokenized equities](https://docs.chain.link/data-feeds/tokenized-equity-feeds/robinhood), [Robinhood Stock Token APIs](https://docs.robinhood.com/chain/stock-token-apis/), [Chainlink feed directory](https://reference-data-directory.vercel.app/feeds-robinhood-mainnet.json), [Robinhood protocol contracts](h","treeHash":null,"usage":{"cachedInputTokens":3307712,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":82516,"runtime":"claude","turns":52,"wallClockMs":1356214}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"771f83f312eace21","findings":[],"hash":"6f04f289688839cab5c86cfbedcf4bc5ff04771608a073b6cc048eca578c4954","nodeId":"15736f14-ba66-4ce1-ab31-608fc3cae79e","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":1,"wallClockMs":18577}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4dd74fd7c315da80","findings":[{"citation":"resolved","description":"_depositState() iterates every listed asset and returns InvalidPrice for any asset with managed != 0 whose _priceOK() fails, and Unreadable (line 554) for any listed asset, even with managed == 0, whose balanceOf cannot be read. _priceOK() calls IStockToken.oraclePaused() on the token itself, so an issuer pausing the oracle of one held stock (a routine action when the underlying is halted or delisted) makes deposit() of every other asset revert. Nothing can clear the condition: closeAsset() leaves the asset in the loop; a Feed or Band proposal does not touch oraclePaused; there is no delist; managed never returns to zero because the 1e15 dead shares keep a pro-rata residue (50005012531328321 wei after a full exit in the reproduction); recognizeLoss() needs an actual balance shortfall; flagDeficit() reverts with InvalidInput when there is none. The same permanent state is reached when a Stock Token is upgraded so balanceOf reverts or returns non-32-byte data (Unreadable at line 554), which also applies to listed assets that were never deposited, and when a delisted stock's Chainlink feed stops updating (26-hour bound), where the only exit is the owner proposing a feed that is not the true feed. Redeem and claim are unaffected, so funds are safe, but the vault silently becomes redeem-only forever, which contradicts the index-vault purpose and the operator playbook in the README (pause/close) that cannot help here. Fix: skip (not fail on) assets that are closed and have managed == 0 in the deposit health loop, and give a closed asset with managed below a dust threshold, or with oraclePaused true for longer than the proposal delay, a timelocked, guardian-vetoable delisting path that moves its managed residue to totalOwed-style accounting; at minimum let a Reopen/Close state exclude a closed asset's price from the deposit gate while still paying it on redeem.","line":558,"path":"src/BaskVault.sol","reproduction":"Genesis with 3 assets, deposits open. Alice deposits 10e18 of asset0 and 10e18 of asset1. Issuer sets asset0.oraclePaused() = true. Guardian closeAsset(asset0). Expected: deposits into asset1 and asset2 continue, since asset0 is closed. Actual: deposit(asset1,...) and deposit(asset2,...) both revert DepositUnavailable(InvalidPrice, asset0); depositStatus(asset1) returns (10, asset0). Owner then proposes a replacement feed and a band re-centre, both execute after 7 days, and deposits still revert with the same error. flagDeficit(asset0) reverts InvalidInput. After Alice redeems 100% of her shares managed[asset0] is still 50005012531328321 wei so the InvalidPrice branch stays active forever. Variant: vm.etch(asset2, \"\") (token bricked by upgrade) with managed[asset2] == 0 makes every deposit revert DepositUnavailable(Unreadable, asset2) and flagDeficit(asset2) revert TransferFailed.","severity":"medium","snippet":"                if (!_priceOK(a, reads[i], answers[i], times[i])) return (Reason.InvalidPrice, a.token, 0, 0);","title":"One managed asset with a paused oracle, dead feed or unreadable balance disables every deposit permanently; no role has a remedy"},{"citation":"resolved","description":"The brief states that owner changes wait 7 days and the guardian can veto. Two owner actions that change where value flows bypass the proposal machinery entirely: setFeeRecipient() (lines 262-268) and transferOwnership()/acceptOwnership() (lines 247-260). While feeRecipient is unset, the 0.5% entry fee is not minted and the 0.5% exit fee is burned, so both accrue to existing BASK holders; the moment setFeeRecipient() is mined every subsequent fee is minted or transferred to the recipient instead. A compromised or hostile owner key can redirect this flow (and hand the owner role to a new key) in one transaction, with no window in which the guardian can cancel, which is the Trust Gap pattern of an admin setter whose write moment changes the destination of economic value. Impact is bounded to the fee stream (about 1% of round-trip volume) and the design document describes the action as immediate and once, so this is reported as a spec/trust mismatch rather than a bypass. Fix if the 7-day rule is intended to cover it: route setFeeRecipient (and optionally ownership handover) through _propose() as a new Kind so the guardian can cancel it, or document the exception explicitly in the brief and README.","line":262,"path":"src/BaskVault.sol","reproduction":"Alice deposits 10e18 of asset0 (feeRecipient unset, fee shares not minted, totalSupply 995e18). In the same block the owner calls setFeeRecipient(OWNER): it succeeds, proposalCount stays 0, no ProposalCreated event, guardian has nothing to cancel. Bob then deposits 10e18 of asset1 and 4975000000000000000 BASK fee shares are minted to OWNER immediately. Expected under the stated governance model: a 7-day proposal the guardian can veto. Actual: immediate effect.","severity":"low","snippet":"    function setFeeRecipient(address recipient) external nonReentrant onlyOwner {","title":"setFeeRecipient and ownership transfer take effect immediately with no 7-day delay and no guardian veto"},{"citation":"resolved","description":"redeem() computes each leg from min(managed, balance - totalOwed), so shareholders are junior to deferred creditors (line 488). claim() caps the payment only by the raw vault balance and never by balance - managed, so creditors are senior to shareholders and may consume the tokens that back managed. The two sides of the deposit/redeem/claim pair therefore apply reservation asymmetrically. When an issuer burns or seizes part of the vault's balance after some redemptions were deferred (paused token, blacklisted recipient), the first creditor to claim takes 100% of what is left and remaining holders absorb the whole loss, while if the same creditor had been paid on time the loss would have been pro-rata. The README documents claims as served in transaction order without haircut, so this is reported as a design asymmetry with a concrete loss-distribution effect rather than a bypass. Fix options: cap claim at max(0, balance - managed[token]) plus the creditor's pro-rata share of any shortfall, or when balance < managed + totalOwed, scale both sides by the same ratio.","line":744,"path":"src/BaskVault.sol","reproduction":"Alice and Bob each deposit 10e18 of asset0 (managed 20e18, balance 20e18). Issuer pauses transfers; Alice redeems all her shares: her leg 9974927318295739348 is deferred (owed), managed becomes 10025072681704260652, balance stays 20e18. Issuer seizes 12e18 from the vault: balance 8e18. Alice claims: paid 8000000000000000000, balance 0, managed still 10025072681704260652. Bob redeems all his shares and receives 0 of asset0. Expected pro-rata outcome with 8e18 left for ~20e18 of claims: roughly 4e18 each. Actual: Alice 8e18, Bob 0.","severity":"low","snippet":"        if (balance < amount) amount = balance;","title":"Creditors (owed) are paid out of collateral backing managed shares, while redeemers are paid only after reserving totalOwed: an issuer seizure falls entirely on remaining holders"},{"citation":"resolved","description":"recognizeLoss() permanently reduces managed[token]. Every payout path (redeem legs at line 718-719, NAV at line 555) is bounded by managed, and the README states direct transfers are ignored and there is no sweep. If an issuer later reverses a seizure or un-burns (restores) the vault's balance, the restored tokens are above managed and can never be paid to anyone. The vault has deposit-side logic to absorb recovery before recognition (deposit clears deficit records), but no symmetric path after recognition, so a temporary issuer action that is later corrected converts into a permanent loss for holders and a permanent idle balance in the vault. Fix: add a permissionless recognizeRecovery(token) that raises managed[token] toward min(balance - totalOwed, managedBefore + previously recognized loss) (tracked in a lossHistory mapping), or allow redeem legs to use min(available, managed + recognizedLoss[token]).","line":769,"path":"src/BaskVault.sol","reproduction":"Alice deposits 10e18 of asset0. Issuer burns 10e18 from the vault; anyone flagDeficit(asset0); after 7 days recognizeLoss(asset0) sets managed to 0. Issuer restores 10e18 to the vault (balance 10e18, managed 0, totalOwed 0). Alice redeems 100% of her shares: leg for asset0 is 0 and the vault still holds 10e18 of asset0 after she exits. Expected: the restored 10e18 is redeemable by shareholders. Actual: stranded; and because NAV is 0 with supply > 0, depositStatus returns ZeroNAV (12), so the vault is also closed to deposits.","severity":"low","snippet":"        managed[token] -= loss;","title":"Collateral restored after recognizeLoss is stranded forever: no path re-recognizes recovered balance"},{"citation":"resolved","description":"deposit() rejects receiver == address(this) (line 631) but _transfer() only rejects address(0). BASK sent to the vault address can never be redeemed (the vault never calls redeem on itself and has no sweep), so the backing of those shares accrues to nobody. Minor inconsistency in the same guard pair; fix by adding to == address(this) to the _transfer check.","line":227,"path":"src/BaskVault.sol","reproduction":"Alice deposits and holds 994999000000000000000 BASK. Alice calls transfer(address(vault), 1e18): succeeds, balanceOf(vault) == 1e18, totalSupply unchanged. Expected (by analogy with deposit's receiver check): revert InvalidAddress. Actual: shares locked in the vault forever.","severity":"info","snippet":"        if (to == address(0)) revert InvalidAddress();","title":"BASK transfer()/transferFrom() accept the vault itself as recipient while deposit() rejects it; shares sent to the vault are irrecoverable"}],"hash":"7337186d87d5152589fff676295d502ff9055ca77cce7a3426e240275580f98b","nodeId":"c7aa6506-3047-48bc-946a-c009197a7313","outcome":"completed","summary":"The review is complete. The findings file is in place and valid, and the working tree holds only ignored build artifacts and the findings file.\n\n## Outcome\n\nI audited `src/BaskVault.sol` against the Access Control, Trust Gap and Asymmetry guides, traced every state-changing entry point, ran the existing 63-test suite (all pass), and verified the live deployment on Robinhood Chain: the deployed runtime bytecode matches a local build byte for byte, owner and guardian match the launch manifest, the factory proxy's implementation carries `tokenAddress(bytes32)`, and a real Stock Token (TSLA, a beacon proxy) reports 18 decimals, a `uid()` that round-trips through the factory, and a working `oraclePaused()`. Its `balanceOf` costs about 12k gas, well under the 50k bound.\n\n**No critical or high defects.** The four hard-look areas held up:\n\n- **Redeem and claim cannot be blocked.** Redeem reads no feeds, no pause flags, no gate. Every token call is gas-bounded, the self-call rolls back lying transfers, and `managed`/`owed`/`totalOwed` conserve under all hostile modes. Worst-case 64-asset redeem measured 22.7M gas, under the 32M per-transaction cap of an Arbitrum-based chain.\n- **No asset or mint leak.** `payLeg` is self-only, all mutators are `nonReentrant` (reentry blocked from all 25 entry points), proposals re-check conditions at execution, and nothing in `closeAsset`, `recognizeLoss`, `setFeeRecipient` or `finalizeGenesis` moves tokens or mints.\n- **Share math** rounds in the vault's favour everywhere, NAV is `managed`-based so donations are inert, and the dead shares cannot be inflated.\n- **Gates and caps** share one code path, so none can be bypassed.\n\n## Findings written to `.imd-findings.json`\n\n1. **Medium.** One managed asset whose issuer pauses its oracle, whose feed dies, or whose code becomes unreadable blocks every deposit forever. Close, feed replacement and band proposals do not help, there is no delist, and dead shares keep `managed` above zero. Redeem still works, so funds are safe but the vault turns redeem-only.\n2. **Low.** `setFeeRecipient` and ownership handover are immediate with no guardian veto, contrary to the brief's 7-day rule. Impact is the fee stream only.\n3. **Low.** Claims are paid from collateral backing `managed` while redeems reserve `totalOwed` first. After an issuer seizure, the first creditor takes everything and remaining holders absorb the whole loss.\n4. **Low.** Collateral restored after `recognizeLoss` is stranded with no recovery path.\n5. **Info.** BASK can be transferred to the vault address and locked, while `deposit` rejects that receiver.\n\n## Trust assumptions documented, not defects\n\nThe owner's instant powers are pause, close, lower the NAV cap, set the fee recipient once, and list assets before genesis. Everything else waits 7 days and is vetoable except guardian replacement. A compromised owner can only harm users through a wrong-but-in-band feed, capped at 4x by the band and by the per-asset and daily limits. Stock Token issuers share one beacon, so a single upgrade changes every listed token at once; the vault survives this on exits only as long as `balanceOf` stays under 50k gas.\n\nScratch tests were removed. Mainnet verification used the public RPC at `rpc.mainnet.chain.robinhood.com`; I could not locate a Chainlink feed address on that chain, so the 8-decimal feed assumption remains unverified.","treeHash":null,"usage":{"cachedInputTokens":2888150,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":75722,"runtime":"claude","turns":44,"wallClockMs":1168660}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a18a0c6087e1362f","findings":[{"citation":"resolved","description":"_depositState requires _priceOK for every asset with managed != 0, including closed assets. _priceOK fails when the Stock Token's oraclePaused() returns true, reverts, or returns malformed data, or when the feed is stale/outside the band. Pausing an oracle is a routine issuer action (delisting, corporate action, wind-down), and the issuer is explicitly untrusted for this. Once it happens, deposits into every other asset revert with DepositUnavailable(InvalidPrice, thatToken). No role can recover: closeAsset only clears `open` (not consulted here), proposeFeed cannot fix oraclePaused(), recognizeLoss needs a real balance shortfall, and managed[token] can only fall through redeem legs computed as floor(managed * net / supply), which is strictly less than managed because net < supply (1e15 dead shares plus the rounded-up fee). So managed stays > 0 after every holder exits and the vault is deposit-dead for good, while still charging nothing and serving only redemptions. The README states managed closed assets still need valid prices, but not that the condition is unrecoverable. Fix direction (design decision for the requester): a timelocked, guardian-vetoable proposal (e.g. Kind.Retire) that marks a closed asset as excluded from the deposit price check after its oracle has been invalid for N days, valuing it at zero in NAV while redeem still distributes it pro rata; or allow recognizeLoss-style write-down of managed to zero for a closed asset whose oracle has been paused for N days. Either changes the economics (depositors get slightly more shares) and needs a scope decision.","line":558,"path":"src/BaskVault.sol","reproduction":"State: three genesis assets, deposit 1e18 of stocks[0] (managed = 1e18). Issuer sets stocks[0].oraclePaused() = true. Guardian calls closeAsset(stocks[0]). Then depositStatus(stocks[1]) == (InvalidPrice, stocks[0]) and deposit(stocks[1], 1e18, ...) reverts DepositUnavailable(InvalidPrice, stocks[0]). User redeems all shares: managed[stocks[0]] is left at 5_010_000_000_000_000 wei (never zero), and depositStatus(stocks[1]) is still InvalidPrice. Expected: an operator path to stop a dead asset from gating the whole vault. Actual: every deposit reverts forever unless the issuer unpauses or burns the vault's holding. Scratch test testOraclePausedManagedAssetBlocksAllDepositsNoRemedy in test/scratch/Probe.t.sol logs managed left = 5010000000000000 and status 10 after the full exit.","severity":"medium","snippet":"                if (!_priceOK(a, reads[i], answers[i], times[i])) return (Reason.InvalidPrice, a.token, 0, 0);","title":"One managed asset with a paused or broken oracle halts every deposit permanently; closeAsset has no effect and redemption rounding never empties managed"},{"citation":"resolved","description":"recognizeLoss writes managed down to the available balance, and nothing ever writes it back up except a new deposit of that same token. Redeem pays min(managed, available), so tokens the issuer later returns to the vault (reversal of an erroneous freeze-and-burn, or a reissue) are unreachable by shareholders: only existing owed creditors can claim them. In the total-loss case the problem compounds with line 564 `if (totalSupply != 0 && nav == 0) return (Reason.ZeroNAV, ...)`: the 1e15 dead shares minted on the first deposit can never be burned, so totalSupply != 0 holds forever and nav == 0 blocks every deposit of every asset. The README says a complete loss 'with remaining BASK supply' blocks deposits, but remaining supply is unconditional, so a complete loss bricks the vault permanently even after the issuer makes it whole. Fix: treat nav == 0 with nonzero supply as a fresh start (price shares 1:1 as on the first deposit, leaving the stale shares worthless), and/or let a readable balance above managed be re-recognised into managed for an asset whose deficit was recognised (bounded by the recognised loss) so restored collateral benefits shareholders rather than being dead.","line":769,"path":"src/BaskVault.sol","reproduction":"State: deposit 10e18 of stocks[0]. Issuer burns 10e18 from the vault. Anyone calls flagDeficit(stocks[0]); after 7 days recognizeLoss(stocks[0]) sets managed = 0. Issuer then mints 10e18 back to the vault (balance 10e18). previewRedeem(all user shares) returns leg 0 for stocks[0]; redeem pays 0 and burns the shares; the vault still holds 10e18 with no path out. depositStatus(stocks[0]) then returns ZeroNAV (12) and every deposit reverts, with totalSupply = 1e15 dead shares. Expected: restored collateral reachable and the vault able to restart. Actual: 10e18 stranded and deposits dead. Scratch test testRestoredCollateralAfterRecognitionIsStranded logs paid = 0, vault still holds = 10000000000000000000, deposit status = 12.","severity":"low","snippet":"        managed[token] -= loss;","title":"Collateral restored after recognizeLoss is stranded, and a fully recognized loss leaves deposits blocked by ZeroNAV forever"},{"citation":"resolved","description":"_checkListing verifies decimals(), the factory uid() mapping and the feed, but not that IStockToken(token).oraclePaused() returns a 32-byte word. _priceOK requires exactly that for the incoming asset on every deposit. Listings are permanent (no removal) and genesis listings are immediate, so a token whose implementation lacks or later drops oraclePaused() (issuer upgrade) occupies one of the 64 slots forever, counts toward the 3-feed freshness gate but can never be deposited. If it already has a managed balance when the issuer removes the function, the previous finding applies and every deposit is blocked. Fix: call oraclePaused() in _checkListing with the same 32-byte return check as _priceOK and revert InvalidAsset otherwise, at proposal and at execution.","line":436,"path":"src/BaskVault.sol","reproduction":"Input: a token contract that satisfies decimals() == 18 and the factory mapping but has no oraclePaused() function (StockMock with brokenOracle = true reproduces the reverting case: stocks[0].configure(18, false, true)). proposeAsset(token, feed) succeeds and the asset is listed. Then depositStatus(token) returns (InvalidPrice, token) and deposit(token, 1e18, ...) reverts DepositUnavailable(InvalidPrice, token) on every attempt, as the existing testManagedPriceStalenessAndOraclePause shows for the post-listing case. Expected: listing rejects a token the pricing path cannot evaluate. Actual: it is accepted into a permanent slot.","severity":"low","snippet":"            IStockToken(token).decimals() != 18","title":"Listing never verifies the oraclePaused() interface that pricing requires, so a token without it is listed permanently but can never be deposited"},{"citation":"resolved","description":"deposit rejects receiver == address(this) but _transfer only rejects address(0). Shares sent to the vault cannot be redeemed because redeem burns from msg.sender and the vault never calls itself with that selector; they stay in totalSupply forever and dilute nobody but are lost to the sender. Fix: also revert when to == address(this) in _transfer, matching deposit.","line":227,"path":"src/BaskVault.sol","reproduction":"State: deposit 1e18 of stocks[0] so USER holds shares. USER calls vault.transfer(address(vault), 1e18). Expected: revert InvalidAddress, consistent with deposit. Actual: succeeds; vault.balanceOf(address(vault)) == 1e18 and no function can move or burn them. Scratch test testSharesTransferredToVaultAreStuck passes on the current code.","severity":"info","snippet":"        if (to == address(0)) revert InvalidAddress();","title":"BASK transfer to the vault's own address is accepted while deposit forbids it; such shares are unrecoverable"}],"hash":"85a9a0961a6bd56c5acf1f29568dd572bf71ca1ce22aabd2b3155fbc02475cc7","nodeId":"eb9e94dd-4675-4924-b889-305eb66f38bb","outcome":"completed","summary":"The review is complete. The findings file `.imd-findings.json` is written and valid, the scratch directory is removed, and `git status` shows no stray files (only gitignored build output).\n\n**Outcome.** The four assigned areas hold up. I found no path that blocks or reverts redeem or claim, no path that moves assets or mints BASK outside deposit, redeem and claim, and no bypass of the deposit gate, per-asset cap, daily bucket or NAV cap. The deposit and share math rounds in the vault's favour, NAV counts only `managed` so donations cannot inflate the share price, and FullMath matches the Uniswap reference. I recorded four findings, none critical or high, so no proof tests were attached.\n\n- **Medium, line 558.** A single managed asset whose issuer pauses its oracle blocks every deposit permanently. `closeAsset` does not help, `proposeFeed` cannot fix `oraclePaused()`, and redeem legs floor so `managed` never reaches zero. The probe showed 5.01e15 wei left managed after every holder exited, with deposits still rejected.\n- **Low, line 769.** Collateral the issuer returns after `recognizeLoss` is stranded, since nothing writes `managed` back up. After a total loss, the 1e15 dead shares keep `totalSupply` nonzero forever, so ZeroNAV blocks all deposits even once the vault is made whole.\n- **Low, line 436.** Listing never checks that `oraclePaused()` is callable, though every deposit requires it. A token lacking it takes a permanent slot that can never be deposited.\n- **Info, line 227.** BASK can be transferred to the vault's own address and is then unrecoverable, while deposit rejects that receiver.\n\n**Coverage.** I traced every external function, the self-call gas budgets, the 50,000-gas balance reads, owed and totalOwed conservation, FullMath, the fee rounding, first-deposit and dead-share logic, the caps and bucket decay, and all proposal kinds. All 63 existing tests pass, including the 256-run invariant campaigns. The worst-case 64-asset redeem measured 22.66M gas, which fits a 32M per-transaction limit but should be confirmed against Robinhood Chain's actual limit, which I could not verify offline. Live Stock Token and feed behaviour on chain 4663 was not checked.","treeHash":null,"usage":{"cachedInputTokens":1776046,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":63003,"runtime":"claude","turns":25,"wallClockMs":997885}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"de319b702da6aa2e","findings":[{"citation":"resolved","description":"In redeem (lines 717-719) a token whose balanceOf cannot be read is treated as having available == managed, so the leg is managed*net/supply with no haircut for any shortfall that already exists. payLeg then also fails on the unreadable balance, so the full leg becomes owed[msg.sender] and totalOwed. totalOwed is reserved ahead of managed in _available (line 487-488), and claim pays min(owed, balance) first come first served. So a holder who redeems while an already-short asset is unreadable gets their full nominal entitlement as a senior claim, and the shortfall that should be shared pro rata falls entirely on the holders who stay. This contradicts the pro-rata haircut redeem applies when the same asset is readable (line 719). The recorded deficits[token] is not consulted either. Fix: when the balance is unreadable, cap the leg base at managed minus the last known shortfall (for example deficits[token].amount), or record such legs as pro-rata pool claims that take the same haircut at claim time instead of a fixed senior amount.","line":486,"path":"src/BaskVault.sol","reproduction":"3 genesis assets at $100. ALICE and BOB each deposit 100e18 of each asset, so token0 managed = 200e18. The issuer burns 100e18 of the vault's token0 (real balance 100e18). The issuer then pauses token0 so balanceOf reverts. ALICE redeems all her shares (about 49.8% of supply). Expected: ALICE's token0 leg is about 49.8e18, her pro-rata share of the 100e18 actually held. Actual: owed[ALICE][token0] = 99.63e18. After the pause lifts, claim pays ALICE 99.63e18, which leaves the vault 0.37e18 of token0 against BOB's managed 100.37e18. BOB's full redemption then yields 0.37e18 of token0 where he should get about 50e18. Verified with a scratch Foundry test (logs: alice owed 99627679579114182030, alice claimed 99627679579114182030, bob token0 leg 370458806264264783).","severity":"medium","snippet":"        if (!readable) return (false, managed[token]);","title":"Unreadable short asset: redeem leg ignores the shortfall and becomes senior debt, shifting the whole loss to remaining holders"},{"citation":"resolved","description":"Anyone can call recognizeLoss 7 days after flagDeficit, and it permanently lowers managed. Nothing ever raises managed except deposit's exact transferred amount, and both redeem (line 719) and NAV only use min(available, managed). If an issuer clawback is reversed after the 7-day window, the restored tokens are excess over managed + totalOwed that no redeem or claim can reach, and no rescue exists. Holders permanently lose value the vault custodies. The same applies to any issuer balance increase: a forward stock split or dividend done by raising holder balances while the feed answer is split-adjusted cuts that asset's NAV share and redeem legs by the split ratio, with the rest stuck. Losses can be recognized but recoveries never can. Fix that keeps donations ignored: track recognizedLoss[token] and add a permissionless recoverLoss(token) that adds min(available - managed, recognizedLoss[token]) back to managed (optionally after its own delay). Treat issuer-credited split balances explicitly, for example with a time-locked owner proposal to rebase managed.","line":769,"path":"src/BaskVault.sol","reproduction":"3 genesis assets at $100. ALICE and BOB each deposit 100e18 of each (token0 managed 200e18). The issuer burns 100e18 of the vault's token0. Anyone calls flagDeficit(token0), warps 7 days and calls recognizeLoss(token0), so managed = 100e18. The issuer then re-mints the 100e18 to the vault (balance 200e18). ALICE redeems all her shares. Expected: a token0 leg of about 99.63e18 (pro-rata of 200e18 held). Actual: 49.81e18. After BOB also redeems everything, the vault still holds 100.25e18 token0 with managed 0.25e18 and totalOwed 0, so 100e18 is unreachable forever. Verified with a scratch Foundry test.","severity":"medium","snippet":"        managed[token] -= loss;","title":"recognizeLoss is irreversible: tokens the issuer later restores (or credits by split/multiplier) are permanently stuck and never counted"},{"citation":"resolved","description":"lowerNAVCap takes effect immediately and can set the cap to 0 to stop deposits, but it does not invalidate pending raise proposals. executeProposal is permissionless and only checks that p.value > NAV_CAP, which a lowered cap makes easier to satisfy. A raise queued before an incident therefore re-opens deposit capacity as soon as it matures, and any address can trigger it. Reopen proposals are invalidated by a later close through closeNonce, but cap raises have no equivalent. Fix: keep a capNonce that lowerNAVCap increments, store it in NAVCap proposals, and require equality in _pending (the same pattern as Reopen). Alternatively, cancel pending raises inside lowerNAVCap.","line":399,"path":"src/BaskVault.sol","reproduction":"The owner calls proposeNAVCap(5_000_000e18) at t0. At t0+3d, during an incident, the owner calls lowerNAVCap(0), so NAV_CAP = 0 and deposits stop. At t0+7d an arbitrary address 0xBAD calls executeProposal(id). Expected: the stale raise is invalid after the emergency lowering and NAV_CAP stays 0. Actual: NAV_CAP = 5_000_000e18 and deposits reopen unless the owner or guardian noticed and cancelled the proposal. Verified with a scratch Foundry test.","severity":"low","snippet":"            if (p.value <= NAV_CAP) revert InvalidInput();","title":"Emergency lowerNAVCap is silently undone by an older pending NAVCap raise that anyone can execute"}],"hash":"98538215865f94325b362f7650950584629cb6397486201d98565e10ff6daa04","nodeId":"fd3d54ee-6e3f-449e-a583-c553124f82f7","outcome":"completed","summary":"I found three defects in `src/BaskVault.sol`, two medium and one low, and wrote them to `.imd-findings.json`. Each one was reproduced with a Foundry test in `test/scratch/`, which I then deleted; no project files were changed. None is critical or high, so none carries a proof.\n\n**Findings**\n\n1. **Medium – unreadable short asset (line 486, used by redeem at 717–719).** If a token is already short (the issuer burned part of the vault's balance) and its `balanceOf` then reverts, redeem assumes the vault holds the full `managed` amount. The redeemer's leg becomes full-size `owed` debt, and `totalOwed` is paid before anything else.\n   - **Reproduction:** the issuer burns 100e18 of the 200e18 token0 the vault manages, then pauses `balanceOf`. ALICE redeems her ~49.8% and is owed 99.6e18 instead of about 49.8e18, and claims all of it once the pause lifts.\n   - **Effect:** BOB, who holds the other half, can then redeem only 0.37e18 of token0.\n   - **Fix:** when the balance can't be read, subtract the last known shortfall (e.g. `deficits[token]`) from the leg base.\n\n2. **Medium – `recognizeLoss` can't be undone (line 769).** Anyone can permanently reduce `managed` once a deficit has been flagged for 7 days, and nothing ever adds it back.\n   - **Reproduction:** burn 100e18, flag, wait 7 days, recognize the loss, then the issuer re-mints the 100e18. After every holder redeems, 100e18 of token0 is still in the vault with no one able to withdraw it.\n   - **Same cause:** a stock split done by raising balances while the price feed is adjusted would strand the extra tokens the same way.\n   - **Fix:** record recognized losses per token and add a permissionless function that restores `min(surplus, recognizedLoss)` to `managed`. This keeps direct transfers ignored.\n\n3. **Low – a lowered NAV cap can be raised again by anyone (line 399).** A raise proposed before an emergency `lowerNAVCap(0)` stays valid, and anyone can execute it once it is ready.\n   - **Reproduction:** propose a $5M cap, lower the cap to 0 three days later, then any address executes the proposal at day 7 and deposits reopen.\n   - **Fix:** invalidate pending raises when the cap is lowered, using a nonce the same way a close cancels earlier reopen proposals.\n\n**What held up under review**\n- **Redeem can't be blocked or made to revert.** No owner or guardian action, token behaviour (paused, blacklisted, reverting, gas-burning, lying, no code), feed state, deposit hours, cap or daily limit stops it. The 250,000-gas leg and 50,000-gas balance read can't be starved through the 63/64 gas-forwarding rule, because the redeem would then run out of gas before it could record the debt. The worst-case 64-asset redeem uses 22.7M gas.\n- **Claim reverts only when the token itself can't be paid out,** and the debt is kept.\n- **No path moves assets out or mints BASK except the intended ones.** I checked the proposals, `executeProposal`, `closeAsset`, `recognizeLoss`, `setFeeRecipient`, `finalizeGenesis`, `payLeg` (callable only by the vault) and reentrancy.\n- **Share math is sound.** `FullMath` matches the standard algorithm. Rounding favours the vault on both deposit and redeem. The 1e15 dead shares plus ignoring direct transfers make first-deposit and donation attacks unprofitable.\n- **The deposit gate and limits can't be bypassed.** This covers the gate, the 5% per-asset limit, the daily bucket and the NAV cap.\n\n**Documented design choices, not reported:** ownership transfer, unpausing deposits and setting the fee recipient take effect immediately, and the guardian can't veto its own replacement. The README describes all of these as intended.\n\nThe original 63 tests still pass.","treeHash":null,"usage":{"cachedInputTokens":855735,"inputTokens":20,"model":"claude-opus-5-5","outputTokens":56640,"runtime":"claude","turns":12,"wallClockMs":646445}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"9e51ef2afd7c2af8","findings":[{"citation":"resolved","description":"_depositState() requires _priceOK() for every asset with managed != 0 (line 557-558) and a readable balance for every listed asset even with managed == 0 (line 554). _priceOK() calls IStockToken.oraclePaused() on the token itself and requires an answer inside the static 4x band no older than 26 hours. The issuer is untrusted and can pause an oracle, upgrade a token so balanceOf reverts or returns non-32-byte data, or a delisted stock's Chainlink feed can simply stop updating. Once that happens to one held asset, deposit() of every other asset reverts with DepositUnavailable(InvalidPrice|Unreadable, thatToken). Nothing clears it: closeAsset() only clears `open`, which this loop does not consult; a Band proposal reverts on a stale feed (answer must be < 26h old) and cannot change oraclePaused; a Feed proposal must stay within the current band and be readable, which for a dead stock means pairing it with a feed that is not its true feed; flagDeficit() reverts InvalidInput when there is no balance shortfall; and managed never returns to zero through redemptions because each leg is floor(managed*net/supply) with net < supply (1e15 dead shares plus the rounded-up fee), so after every holder exits a residue remains (5_010_000_000_000_000 wei in the reproduction). Redeem and claim are unaffected, so funds are safe, but the vault silently becomes redeem-only for its whole life. The README documents that closed managed assets still need valid prices, but not that the condition is unrecoverable; the operator playbook (pause/close) cannot help. Merged from audit_math, audit_permissions and audit_economics. Fix (design decision for the requester): a timelocked, guardian-vetoable Retire kind that excludes a closed asset from the deposit price/readability loop (valuing it at zero in NAV while redeem still pays it pro rata via min(managed, available)), and skip closed assets with managed == 0 in the loop. Any choice slightly changes deposit pricing and needs a scope decision.","line":558,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS1_OraclePausedManagedAssetBlocksAllDeposits (passes on current code, logs the defect): three genesis assets at $100, USER deposits 1e18 of stocks[0]. Issuer sets stocks[0].oraclePaused()=true (StockMock.configure(18,true,false)); guardian closeAsset(stocks[0]). depositStatus(stocks[1]) == (InvalidPrice=10, stocks[0]); deposit(stocks[1],1e18,...) reverts DepositUnavailable(10, stocks[0]). Owner proposeBand(stocks[0]) + execute after 7 days: status unchanged. flagDeficit(stocks[0]) reverts InvalidInput. USER redeems 100% of shares: managed[stocks[0]] == 5010000000000000 (non-zero), totalSupply == 1e15, depositStatus(stocks[1]) still (10, stocks[0]). Expected: an operator path to stop a dead asset from gating the vault. Actual: deposits revert forever. testS1b (4 assets): frozen feed on closed managed stocks[0] gives (10, stocks[0]) and executeProposal(Band) reverts InvalidFeed(feed0); vm.etch(stocks[2], \"\") with managed[stocks[2]] == 0 gives (Unreadable=6, stocks[2]) for every deposit and flagDeficit(stocks[2]) reverts TransferFailed.","severity":"medium","snippet":"                if (!_priceOK(a, reads[i], answers[i], times[i])) return (Reason.InvalidPrice, a.token, 0, 0);","title":"One managed asset whose oracle is paused, feed dead or balance unreadable disables every deposit permanently; no role has a remedy"},{"citation":"resolved","description":"recognizeLoss() is the only path that reconciles managed[token] downward to the physical balance, and deposit() is the only path that raises it, by exactly the transferred amount. Redeem legs (line 718-719), previewRedeem and NAV all use min(available, managed), claim pays only owed balances, and the README states there is no sweep or rescue. So if a Stock Token issuer burns or seizes part of the vault's holding, anyone recognizes the loss after 7 days, and the issuer later returns the tokens (reversal of an erroneous compliance freeze, reissue after a migration), the returned tokens sit above managed as an unmanaged surplus: excluded from NAV, never paid by any redemption, flagDeficit reverts InvalidInput, no function can raise managed. The holders who absorbed the loss permanently lose the restored value. The same mechanism applies to any issuer balance increase that is not a vault deposit, for example a forward stock split or stock dividend effected by raising holder balances while the feed answer is split-adjusted: that asset's NAV share and redeem legs fall by the split ratio and the rest is stuck (we could not verify how the live issuer implements corporate actions; the brief's upgrade power allows it). Merged from audit_math, audit_permissions, audit_economics and audit_flow. Fix that keeps donations ignored: track recognizedLoss[token] and add a permissionless recognizeRecovery(token) that raises managed[token] by min(balance - totalOwed - managed, recognizedLoss[token]) (optionally after its own delay); issuer-credited split balances need an explicit timelocked owner proposal to rebase managed.","line":769,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS2_RestoredAfterRecognizeLossStranded (passes on current code, logs the defect): USER deposits 10e18 of stocks[0] (managed 10e18). Issuer burns 4e18 from the vault (balance 6e18). flagDeficit(stocks[0]); warp 7 days; recognizeLoss(stocks[0]) -> managed 6e18. Issuer mints 4e18 back (balance 10e18, managed 6e18). flagDeficit reverts InvalidInput. USER redeems 100% of shares: paid 5969994000000000000 of stocks[0]; vault still holds 4030006000000000000 with no path out. Expected: once the issuer restores the tokens the holders can recover them (full exit pays about 9.95e18). Actual: 4e18 stranded permanently.","severity":"medium","snippet":"        managed[token] -= loss;","title":"recognizeLoss is irreversible: collateral the issuer later restores (or credits by a balance-adjusting split) is stranded forever and never counted"},{"citation":"resolved","description":"When balanceOf is unreadable (reverts, wrong return size, or exceeds the 50,000-gas budget after an issuer upgrade or pause), _available() substitutes managed[token] for the real balance, so redeem() (line 717-720) computes the leg as managed*net/supply with no haircut for a shortfall that already exists, and the recorded deficits[token] is not consulted. payLeg() then also fails on the unreadable balance, so the whole nominal leg becomes owed[msg.sender] and totalOwed. totalOwed is reserved ahead of managed for later redeemers (line 487-488) and claim() pays min(owed, balance) first come first served (line 744). Result: a holder who redeems while an already-short asset is unreadable converts the pro-rata haircut every other holder takes into a senior claim on the full nominal amount, and the holders who stay absorb the entire issuer loss. The same asset, readable, would have been split pro rata (line 719). Any holder who observes the burn plus the unreadable window can front-run the rest by exiting. Preconditions are issuer actions (burn plus unreadable balance), both listed in the brief as issuer powers. Owed/totalOwed stay internally consistent; the distribution is what breaks. Merged from audit_economics and audit_flow. Fix: when the balance is unreadable, cap the leg base at managed minus the recorded deficits[token].amount (or defer the leg without fixing its size and settle it pro rata at claim time when the balance is readable), or cap claim() at balance minus the pro-rata share still owed to managed.","line":486,"path":"src/BaskVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {BaskVault} from \"src/BaskVault.sol\";\n\n/// Finding: when a Stock Token's balance is unreadable, redeem books the leg from `managed`\n/// with no haircut for an existing shortfall and the leg becomes senior debt. The first\n/// creditor to claim takes the whole remaining balance and the remaining holder gets nothing,\n/// although both held equal shares of the same asset.\ncontract FactoryStub {\n    mapping(bytes32 => address) public tokenAddress;\n\n    function set(bytes32 id, address token) external {\n        tokenAddress[id] = token;\n    }\n}\n\ncontract FeedStub {\n    uint8 public constant decimals = 8;\n    address public constant aggregator = address(1);\n    uint256 public updatedAt;\n\n    function touch() external {\n        updatedAt = block.timestamp;\n    }\n\n    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {\n        return (1, 100e8, updatedAt, updatedAt, 1);\n    }\n}\n\ncontract StockStub {\n    bytes32 public uid;\n    uint8 public constant decimals = 18;\n    bool public unreadable;\n    mapping(address => uint256) public bal;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(bytes32 id) {\n        uid = id;\n    }\n\n    function oraclePaused() external pure returns (bool) {\n        return false;\n    }\n\n    function setUnreadable(bool v) external {\n        unreadable = v;\n    }\n\n    function mint(address to, uint256 amount) external {\n        bal[to] += amount;\n    }\n\n    function burn(address from, uint256 amount) external {\n        bal[from] -= amount;\n    }\n\n    function balanceOf(address holder) external view returns (uint256) {\n        require(!unreadable, \"unreadable\");\n        return bal[holder];\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        bal[from] -= amount;\n        bal[to] += amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        require(!unreadable, \"unreadable\");\n        bal[msg.sender] -= amount;\n        bal[to] += amount;\n        return true;\n    }\n}\n\ncontract UnreadableShortSeniorityTest is Test {\n    address constant OWNER = address(0xA11CE);\n    address constant GUARDIAN = address(0x6A5D);\n    address constant ALICE = address(0xA1);\n    address constant BOB = address(0xB0B);\n    // Monday 2026-01-05 16:00 UTC, inside the deposit window.\n    uint256 constant MONDAY = 1_767_628_800;\n\n    BaskVault vault;\n    StockStub[3] stocks;\n    FeedStub[3] feeds;\n\n    function setUp() public {\n        vm.chainId(4663);\n        vm.warp(MONDAY - 4 days);\n        vault = new BaskVault(OWNER, GUARDIAN);\n        FactoryStub impl = new FactoryStub();\n        vm.etch(vault.STOCK_FACTORY(), address(impl).code);\n        FactoryStub factory = FactoryStub(vault.STOCK_FACTORY());\n        for (uint256 i; i < 3; ++i) {\n            stocks[i] = new StockStub(bytes32(i + 1));\n            feeds[i] = new FeedStub();\n            feeds[i].touch();\n            factory.set(stocks[i].uid(), address(stocks[i]));\n            vm.prank(OWNER);\n            vault.proposeAsset(address(stocks[i]), address(feeds[i]));\n        }\n        vm.prank(OWNER);\n        vault.finalizeGenesis();\n        vm.warp(MONDAY);\n        for (uint256 i; i < 3; ++i) {\n            feeds[i].touch();\n        }\n        _fund(ALICE);\n        _fund(BOB);\n    }\n\n    function _fund(address who) internal {\n        stocks[0].mint(who, 100e18);\n        vm.prank(who);\n        stocks[0].approve(address(vault), type(uint256).max);\n    }\n\n    function _deposit(address who, uint256 amount) internal {\n        vm.prank(who);\n        vault.deposit(address(stocks[0]), amount, who, 0, block.timestamp);\n    }\n\n    function _redeemAll(address who) internal returns (uint256[] memory amounts) {\n        uint256 shares = vault.balanceOf(who);\n        vm.prank(who);\n        amounts = vault.redeem(shares, new uint256[](0), block.timestamp);\n    }\n\n    function testEqualHoldersShareAnIssuerShortfallEvenAcrossAnUnreadableWindow() public {\n        address token = address(stocks[0]);\n        _deposit(ALICE, 10e18);\n        _deposit(BOB, 10e18);\n        assertEq(vault.managed(token), 20e18);\n\n        // Issuer burns 16e18 of the vault's holding: only 4e18 remain for ~20e18 managed.\n        stocks[0].burn(address(vault), 16e18);\n        vault.flagDeficit(token);\n\n        // Issuer upgrade/pause makes balanceOf revert while Alice redeems everything.\n        stocks[0].setUnreadable(true);\n        uint256[] memory aliceLegs = _redeemAll(ALICE);\n        uint256 aliceOwed = vault.owed(ALICE, token);\n        assertEq(aliceOwed, aliceLegs[0]);\n\n        // Balance becomes readable again; Alice claims, then Bob redeems everything.\n        stocks[0].setUnreadable(false);\n        vm.prank(ALICE);\n        uint256 aliceClaimed = vault.claim(token, ALICE);\n        uint256 bobBefore = stocks[0].bal(BOB);\n        _redeemAll(BOB);\n        uint256 bobGot = stocks[0].bal(BOB) - bobBefore + vault.owed(BOB, token);\n\n        emit log_named_uint(\"alice owed\", aliceOwed);\n        emit log_named_uint(\"alice claimed\", aliceClaimed);\n        emit log_named_uint(\"bob received\", bobGot);\n\n        // Expected: a shortfall on an asset both hold equally is shared; Bob must not be\n        // wiped out merely because Alice redeemed while the balance was unreadable.\n        assertGt(bobGot, 0, \"remaining holder received nothing of the asset\");\n        // And Alice's claim must not consume the entire remaining balance of the asset.\n        assertLt(aliceClaimed, 4e18, \"unreadable redeemer drained the asset\");\n    }\n}","reproduction":"Proof test test/scratch/UnreadableShortSeniority.t.sol (fails on current code): ALICE and BOB each deposit 10e18 of token0 at $100 (managed 20e18). Issuer burns 16e18 from the vault (balance 4e18); flagDeficit(token0). balanceOf is made to revert. ALICE redeems all her shares: leg booked as owed = 9974927318295739348, managed falls to about 10.025e18. balanceOf readable again. ALICE claim(token0) pays 4000000000000000000 (entire remaining balance). BOB redeems all his shares and receives 0 of token0 (available 0). Expected: each of two equal holders receives about 2e18 of the remaining 4e18, as they would if the balance had been readable at ALICE's redemption. Actual: ALICE 4e18, BOB 0. Also reproduced in test/scratch/Judge.t.sol::testS3_UnreadableShortOverCredit.","severity":"medium","snippet":"        if (!readable) return (false, managed[token]);","title":"Unreadable balance during a shortfall books the redeem leg from managed with no haircut and makes it senior debt; first claimant drains the asset, remaining holders get nothing"},{"citation":"resolved","description":"redeem() computes each leg from min(managed, balance - totalOwed) (line 487-488, 718-719), so shareholders are junior to deferred creditors, but claim() caps the payment only by the raw vault balance and never by balance - managed, so creditors may consume the tokens that back managed. In a healthy state balance == managed + totalOwed and the asymmetry is invisible. When an issuer burns or seizes part of the vault's balance after some payments were deferred (token paused, recipient blacklisted), the first creditor to claim takes 100% of what is left and the remaining holders absorb the whole loss; had the same creditor been paid on time, the later loss would have been shared pro rata. The README documents that underfunded claims are served in transaction order without a haircut, so this is reported as a documented design asymmetry with a concrete loss-distribution consequence rather than a bypass; it is also the reason the unreadable-balance finding at line 486 is harmful. Reported by audit_permissions. Fix options if pro-rata loss sharing is intended: cap claim at max(0, balance - managed[token]) plus the creditor's pro-rata share of any shortfall, or when balance < managed + totalOwed scale both sides by the same ratio.","line":744,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS4_ClaimSeniorToManaged (passes on current code, logs the defect): USER and OTHER each deposit 10e18 of stocks[0] (managed 20e18, balance 20e18). Issuer pauses transfers (StockMock.modes(1,0)); USER redeems all shares: leg 9974927318295739348 deferred to owed, managed about 10.025e18, balance 20e18. Transfers resume; issuer burns 12e18 from the vault (balance 8e18). USER claim(stocks[0]) pays 8000000000000000000. OTHER redeems all shares and receives 0 of stocks[0]. Expected pro-rata outcome for ~20e18 of claims on 8e18: roughly 4e18 each. Actual: 8e18 and 0.","severity":"low","snippet":"        if (balance < amount) amount = balance;","title":"Claims are paid from collateral backing managed shares while redeemers are paid after reserving totalOwed: an issuer seizure after a deferred payment falls entirely on remaining holders"},{"citation":"resolved","description":"After recognizeLoss() writes every managed balance to zero, nav == 0 while totalSupply >= LOCKED_SHARES permanently (the dead shares minted to 0xdEaD on the first deposit can never be burned), so line 564 returns ZeroNAV for every asset and every deposit reverts for the rest of the vault's life, even after all holders exit and even if the issuer later makes the vault whole. The README says a complete loss 'with remaining BASK supply' blocks deposits, but remaining supply is unconditional. This is most likely early in the vault's life when only one asset has been deposited. Reported by audit_math and audit_permissions as part of the recognizeLoss finding; split out because the fix differs. Fix: when nav == 0 and totalSupply != 0, price the deposit as a fresh start (shares = value, as on the first deposit) leaving the stale shares worthless, or otherwise allow a restart.","line":564,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS2b_TotalLossZeroNAVForever (passes on current code, logs the defect): USER deposits 10e18 of stocks[0]. Issuer burns 10e18 from the vault. flagDeficit; warp 7 days; recognizeLoss -> managed 0. Issuer mints 10e18 back to the vault. USER redeems 100%: paid 0; totalSupply == 1e15 == balanceOf(0xdEaD). depositStatus(stocks[1]) == ZeroNAV (12) and deposit(stocks[1], 1e18, ...) reverts DepositUnavailable(12, 0x0). Expected: a vault with no live shares can accept a first-style deposit again. Actual: deposits are dead forever.","severity":"low","snippet":"        if (totalSupply != 0 && nav == 0) return (Reason.ZeroNAV, address(0), 0, 0);","title":"A complete recognized loss leaves deposits blocked by ZeroNAV forever because the 1e15 dead shares never leave totalSupply"},{"citation":"resolved","description":"The global bucket is increased by every successful deposit and only decays linearly over 24 hours; redeem() never reduces it. An actor can fill the entire bucket, max(NAV/4, $100,000), and redeem in the same block, keeping the bucket full while holding no position. All other users are then limited to the decayed slice (1/24 of the cap per hour). The cost is the round-trip fee: 0.5% exit plus 0.5% entry, and with no fee recipient set the entry fee is not minted and the exit fee is burned, so a griefer who is the dominant holder loses only about 0.5%. At the initial $100k floor that is roughly $500-$1,000 per day of deposit denial against a vault with a $1M NAV cap. Documented ('redemptions do not reduce it'), but the consequence is a cheap deposit DoS with no inventory risk. Reported by audit_economics. Fix options that keep the rate-limit intent: subtract the redeemed value (at the NAV used by the redeemer's deposit, or at current managed-based NAV) from the bucket, or track a per-depositor bucket in addition to the global one.","line":611,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS5_BucketGrief (passes on current code, logs the cost): 5 genesis assets at $100, empty vault, bucket cap 100_000e18, per-asset floor $25,000. USER deposits 250e18 ($25,000) into each of assets 0..3 in one block: bucket == 100_000e18. OTHER's deposit of 1e18 ($100) into asset 4 reverts DepositUnavailable(BucketCap=15, stocks[4]). USER redeems all shares in the same block: decayedBucket() still 100_000e18 and OTHER's deposit still reverts with BucketCap. Griefer's total token cost: 5000010054366946976 wei of $100 tokens (about $500). One hour later decayedBucket() == 95833333333333333333334, so everyone combined may deposit at most about $4,166.","severity":"low","snippet":"        nextBucket = decayedBucket() + value;","title":"Daily bucket counts deposits but not redemptions, so one actor can keep all deposits blocked for about 1% of the cap per day"},{"citation":"resolved","description":"lowerNAVCap() takes effect immediately and can set the cap to zero to stop deposits, but it does not invalidate pending raise proposals. executeProposal() is permissionless and only checks p.value > NAV_CAP, which a lowered cap makes easier to satisfy. A raise queued before an incident therefore re-opens deposit capacity as soon as it matures and any address can trigger it, unless the owner or guardian remembers to cancel it. Reopen proposals are invalidated by a later close through closeNonce; cap raises have no equivalent. Reported by audit_flow. Fix: keep a capNonce that lowerNAVCap increments, store it in NAVCap proposals and require equality in _pending (the Reopen pattern), or cancel pending NAVCap proposals inside lowerNAVCap.","line":399,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS6_LowerCapUndoneByPendingRaise (passes on current code, logs the result): owner proposeNAVCap(5_000_000e18) at t0 (id 1). At t0+3d owner lowerNAVCap(0): NAV_CAP == 0. At t0+7d address 0xBAD calls executeProposal(1): succeeds. Expected: the stale raise is invalid after the emergency lowering and NAV_CAP stays 0. Actual: NAV_CAP == 5000000000000000000000000 and deposits reopen.","severity":"low","snippet":"            if (p.value <= NAV_CAP) revert InvalidInput();","title":"Emergency lowerNAVCap is silently undone by an older pending NAVCap raise that anyone can execute"},{"citation":"resolved","description":"The brief states that owner changes wait 7 days and the guardian can veto. Two owner actions that change where value and power flow bypass the proposal machinery: setFeeRecipient() (line 262-268) and transferOwnership()/acceptOwnership() (line 247-260). While feeRecipient is unset, the 0.5% entry fee is not minted and the 0.5% exit fee is burned, so both accrue to existing BASK holders; the moment setFeeRecipient() is mined every subsequent fee is minted or transferred to the recipient. A compromised or hostile owner key can redirect that stream and hand the owner role to a new key in one block with no window in which the guardian can cancel. Impact is bounded to the fee stream (about 1% of round-trip volume), and the README documents the action as immediate and once, so this is a specification/trust mismatch rather than a bypass. Immediate owner actions that only restrict (pause, close, lowerNAVCap) are consistent with the brief and not reported. Reported by audit_permissions. Fix if the 7-day rule is meant to cover it: route setFeeRecipient (and optionally ownership handover) through _propose() as a new Kind so the guardian can cancel it; otherwise document the exception in the brief.","line":262,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS7_FeeRecipientImmediate (passes on current code, logs the result): USER deposits 10e18 of stocks[0] with feeRecipient unset (no fee shares minted). Owner calls setFeeRecipient(OWNER): succeeds immediately, proposalCount stays 0, nothing for the guardian to cancel. USER deposits 10e18 of stocks[1]: 4975000000000000000 BASK fee shares are minted to OWNER in that transaction. Expected under the stated governance model: a 7-day proposal the guardian can veto. Actual: immediate effect.","severity":"low","snippet":"    function setFeeRecipient(address recipient) external nonReentrant onlyOwner {","title":"setFeeRecipient and ownership handover take effect without the 7-day delay or guardian veto the brief states for owner changes"},{"citation":"resolved","description":"_checkListing() verifies decimals(), the factory uid() mapping and the feed, but not that IStockToken(token).oraclePaused() returns a 32-byte word, which _priceOK() requires for the incoming asset on every deposit. Listings are permanent and genesis listings are immediate, so a token whose implementation lacks oraclePaused() takes one of the 64 slots and a feed forever, counts toward the 3-feed freshness gate, and can never be deposited. Reported by audit_math. Fix: call oraclePaused() in _checkListing with the same 32-byte return check as _priceOK and revert InvalidAsset otherwise, at proposal and at execution.","line":436,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS9_ListingWithoutOraclePaused (passes on current code): a token satisfying decimals()==18 and the factory mapping whose oraclePaused() reverts (StockMock.configure(18,false,true)) is accepted by proposeAsset during genesis (assetIndex == 3). After finalization and opening, depositStatus(token) == (InvalidPrice=10, token) while depositStatus(stocks[0]) == OK. Expected: listing rejects a token the pricing path cannot evaluate. Actual: accepted into a permanent slot.","severity":"low","snippet":"            IStockToken(token).decimals() != 18","title":"Listing never checks the oraclePaused() interface that pricing requires, so a token without it occupies a permanent slot but can never be deposited"},{"citation":"resolved","description":"deposit() rejects receiver == address(this) (line 631) but _transfer() only rejects address(0). Shares sent to the vault address can never be redeemed: redeem burns from msg.sender and the vault never calls itself with that selector, and there is no sweep. They stay in totalSupply, dilute nobody, and are lost to the sender. Reported by audit_math and audit_permissions. Fix: also revert when to == address(this) in _transfer, matching deposit.","line":227,"path":"src/BaskVault.sol","reproduction":"test/scratch/Judge.t.sol::testS8_SharesToVault (passes on current code): USER deposits 1e18 of stocks[0] and calls vault.transfer(address(vault), 1e18). Expected (by analogy with deposit's receiver check): revert InvalidAddress. Actual: succeeds, vault.balanceOf(address(vault)) == 1e18, no function can move or burn them.","severity":"info","snippet":"        if (to == address(0)) revert InvalidAddress();","title":"BASK transfer()/transferFrom() accept the vault itself as recipient while deposit() rejects it; such shares are unrecoverable"}],"hash":"a870ffb5801849f6547868f465d73320cfda3d0d2bc353ad14ddadff16da063c","nodeId":"15736f14-ba66-4ce1-ab31-608fc3cae79e","outcome":"completed","summary":"The findings file is on disk, and both it and the scratch directory are excluded from git by the repository's own `.git/info/exclude`, so the clean working tree is expected. Nothing else to request.\n\n## Review complete\n\n`.imd-findings.json` holds 10 verified findings. Every specialist scenario was re-run against the code in `test/scratch/Judge.t.sol`, and the one proof attached was confirmed to fail on the current code for the stated reason. No source or configuration file was changed.\n\n**What I kept, after merging duplicates**\n\n- **Medium, deposit liveness brick.** One held asset with a paused oracle, dead feed, or unreadable balance blocks every deposit forever. Closing the asset, band and feed proposals, and flagDeficit all fail to clear it, and managed never reaches zero through redemptions. Merged from three specialists.\n- **Medium, recognizeLoss is irreversible.** Collateral the issuer restores after a recognized loss is stranded with no path back into managed. I added the balance-adjusting stock-split case as a further trigger, stated as unverified against the live issuer. Merged from four specialists.\n- **Medium, unreadable shortfall over-credit.** A holder who redeems while a short asset's balance is unreadable gets a full nominal senior claim and the first claim drains the asset. Two equal holders end up with 4e18 and 0. This one carries a self-contained Foundry proof. Merged from two specialists.\n- **Low.** Claim seniority over managed after a later seizure, permanent ZeroNAV after a total recognized loss, bucket griefing at about $500 per day, a pending NAV-cap raise overriding an emergency lowering, fee-recipient and ownership changes bypassing the stated 7-day veto model, and listing not checking the oraclePaused interface.\n- **Info.** BASK can be transferred to the vault address and is then unrecoverable.\n\n**Verdicts on the specialist set**\n\n- All 16 specialist findings reproduced. None were dropped.\n- The ZeroNAV consequence was split out of the recognizeLoss finding because its fix differs.\n- Severities were normalized where specialists disagreed. The unreadable-shortfall issue went to medium because an unprivileged holder can exploit it once the issuer state exists; the deposit-brick stayed medium because funds remain safe.\n\n**Independent checks that found nothing new**\n\nRedeem and claim cannot be blocked by any role or token behavior I could construct. No mint or asset-outflow path exists outside deposit, redeem and claim. Share math rounds in the vault's favor and the USD-priced first deposit plus dead shares defeats inflation attacks. The deposit gate, per-asset cap, bucket and NAV cap could not be bypassed. The project's own 64-asset worst case measured about 22.7M gas, under its 28M bound.","treeHash":null,"usage":{"cachedInputTokens":1801481,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":49859,"runtime":"claude","turns":26,"wallClockMs":792604}}],"verification":[]}