# Audit report

> 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.
>
> Look hardest at:
>
> 1. 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.
>
> 2. 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.
>
> 3. 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.
>
> 4. Whether the deposit gate, the 5% per-asset limit, the daily bucket or the NAV cap can be bypassed.

| | |
|---|---|
| Repository | https://github.com/identity-md-launches/launch-865-basket.git |
| Commit | `9d5152799041ff6588ed966566722b615cfa11c7` |
| Job | `6f250477-a6e2-4971-9187-a12f96689929` |
| Judged | 2026-10-07 04:30 UTC |
| Findings | 3 medium · 6 low · 1 info |

Four agents audited the code as it is at `9d51527`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: One managed asset whose oracle is paused, feed dead or balance unreadable disables every deposit permanently; no role has a remedy

`src/BaskVault.sol:558`

```
                if (!_priceOK(a, reads[i], answers[i], times[i])) return (Reason.InvalidPrice, a.token, 0, 0);
```

_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.

**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.

### 2. Medium: recognizeLoss is irreversible: collateral the issuer later restores (or credits by a balance-adjusting split) is stranded forever and never counted

`src/BaskVault.sol:769`

```
        managed[token] -= loss;
```

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.

**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.

### 3. Medium: 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

`src/BaskVault.sol:486`

```
        if (!readable) return (false, managed[token]);
```

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.

**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.

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {Test} from "forge-std/Test.sol";
import {BaskVault} from "src/BaskVault.sol";

/// Finding: when a Stock Token's balance is unreadable, redeem books the leg from `managed`
/// with no haircut for an existing shortfall and the leg becomes senior debt. The first
/// creditor to claim takes the whole remaining balance and the remaining holder gets nothing,
/// although both held equal shares of the same asset.
contract FactoryStub {
    mapping(bytes32 => address) public tokenAddress;

    function set(bytes32 id, address token) external {
        tokenAddress[id] = token;
    }
}

contract FeedStub {
    uint8 public constant decimals = 8;
    address public constant aggregator = address(1);
    uint256 public updatedAt;

    function touch() external {
        updatedAt = block.timestamp;
    }

    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
        return (1, 100e8, updatedAt, updatedAt, 1);
    }
}

contract StockStub {
    bytes32 public uid;
    uint8 public constant decimals = 18;
    bool public unreadable;
    mapping(address => uint256) public bal;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(bytes32 id) {
        uid = id;
    }

    function oraclePaused() external pure returns (bool) {
        return false;
    }

    function setUnreadable(bool v) external {
        unreadable = v;
    }

    function mint(address to, uint256 amount) external {
        bal[to] += amount;
    }

    function burn(address from, uint256 amount) external {
        bal[from] -= amount;
    }

    function balanceOf(address holder) external view returns (uint256) {
        require(!unreadable, "unreadable");
        return bal[holder];
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        allowance[from][msg.sender] -= amount;
        bal[from] -= amount;
        bal[to] += amount;
        return true;
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        require(!unreadable, "unreadable");
        bal[msg.sender] -= amount;
        bal[to] += amount;
        return true;
    }
}

contract UnreadableShortSeniorityTest is Test {
    address constant OWNER = address(0xA11CE);
    address constant GUARDIAN = address(0x6A5D);
    address constant ALICE = address(0xA1);
    address constant BOB = address(0xB0B);
    // Monday 2026-01-05 16:00 UTC, inside the deposit window.
    uint256 constant MONDAY = 1_767_628_800;

    BaskVault vault;
    StockStub[3] stocks;
    FeedStub[3] feeds;

    function setUp() public {
        vm.chainId(4663);
        vm.warp(MONDAY - 4 days);
        vault = new BaskVault(OWNER, GUARDIAN);
        FactoryStub impl = new FactoryStub();
        vm.etch(vault.STOCK_FACTORY(), address(impl).code);
        FactoryStub factory = FactoryStub(vault.STOCK_FACTORY());
        for (uint256 i; i < 3; ++i) {
            stocks[i] = new StockStub(bytes32(i + 1));
            feeds[i] = new FeedStub();
            feeds[i].touch();
            factory.set(stocks[i].uid(), address(stocks[i]));
            vm.prank(OWNER);
            vault.proposeAsset(address(stocks[i]), address(feeds[i]));
        }
        vm.prank(OWNER);
        vault.finalizeGenesis();
        vm.warp(MONDAY);
        for (uint256 i; i < 3; ++i) {
            feeds[i].touch();
        }
        _fund(ALICE);
        _fund(BOB);
    }

    function _fund(address who) internal {
        stocks[0].mint(who, 100e18);
        vm.prank(who);
        stocks[0].approve(address(vault), type(uint256).max);
    }

    function _deposit(address who, uint256 amount) internal {
        vm.prank(who);
        vault.deposit(address(stocks[0]), amount, who, 0, block.timestamp);
    }

    function _redeemAll(address who) internal returns (uint256[] memory amounts) {
        uint256 shares = vault.balanceOf(who);
        vm.prank(who);
        amounts = vault.redeem(shares, new uint256[](0), block.timestamp);
    }

    function testEqualHoldersShareAnIssuerShortfallEvenAcrossAnUnreadableWindow() public {
        address token = address(stocks[0]);
        _deposit(ALICE, 10e18);
        _deposit(BOB, 10e18);
        assertEq(vault.managed(token), 20e18);

        // Issuer burns 16e18 of the vault's holding: only 4e18 remain for ~20e18 managed.
        stocks[0].burn(address(vault), 16e18);
        vault.flagDeficit(token);

        // Issuer upgrade/pause makes balanceOf revert while Alice redeems everything.
        stocks[0].setUnreadable(true);
        uint256[] memory aliceLegs = _redeemAll(ALICE);
        uint256 aliceOwed = vault.owed(ALICE, token);
        assertEq(aliceOwed, aliceLegs[0]);

        // Balance becomes readable again; Alice claims, then Bob redeems everything.
        stocks[0].setUnreadable(false);
        vm.prank(ALICE);
        uint256 aliceClaimed = vault.claim(token, ALICE);
        uint256 bobBefore = stocks[0].bal(BOB);
        _redeemAll(BOB);
        uint256 bobGot = stocks[0].bal(BOB) - bobBefore + vault.owed(BOB, token);

        emit log_named_uint("alice owed", aliceOwed);
        emit log_named_uint("alice claimed", aliceClaimed);
        emit log_named_uint("bob received", bobGot);

        // Expected: a shortfall on an asset both hold equally is shared; Bob must not be
        // wiped out merely because Alice redeemed while the balance was unreadable.
        assertGt(bobGot, 0, "remaining holder received nothing of the asset");
        // And Alice's claim must not consume the entire remaining balance of the asset.
        assertLt(aliceClaimed, 4e18, "unreadable redeemer drained the asset");
    }
}
```

### 4. Low: 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

`src/BaskVault.sol:744`

```
        if (balance < amount) amount = balance;
```

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.

**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.

### 5. Low: A complete recognized loss leaves deposits blocked by ZeroNAV forever because the 1e15 dead shares never leave totalSupply

`src/BaskVault.sol:564`

```
        if (totalSupply != 0 && nav == 0) return (Reason.ZeroNAV, address(0), 0, 0);
```

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.

**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.

### 6. Low: Daily bucket counts deposits but not redemptions, so one actor can keep all deposits blocked for about 1% of the cap per day

`src/BaskVault.sol:611`

```
        nextBucket = decayedBucket() + value;
```

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.

**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.

### 7. Low: Emergency lowerNAVCap is silently undone by an older pending NAVCap raise that anyone can execute

`src/BaskVault.sol:399`

```
            if (p.value <= NAV_CAP) revert InvalidInput();
```

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.

**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.

### 8. Low: setFeeRecipient and ownership handover take effect without the 7-day delay or guardian veto the brief states for owner changes

`src/BaskVault.sol:262`

```
    function setFeeRecipient(address recipient) external nonReentrant onlyOwner {
```

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.

**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.

### 9. Low: Listing never checks the oraclePaused() interface that pricing requires, so a token without it occupies a permanent slot but can never be deposited

`src/BaskVault.sol:436`

```
            IStockToken(token).decimals() != 18
```

_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.

**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.

### 10. Info: BASK transfer()/transferFrom() accept the vault itself as recipient while deposit() rejects it; such shares are unrecoverable

`src/BaskVault.sol:227`

```
        if (to == address(0)) revert InvalidAddress();
```

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.

**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.

---

Judge's submission `a870ffb5801849f6547868f465d73320cfda3d0d2bc353ad14ddadff16da063c`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
