# Audit report

> Basket (BASK) is an immutable index vault for Stock Tokens on Robinhood Chain (chain id 4663), deployed at 0x739fD5B653aA092a434534FA1aDE67C1770b5a5B with nothing listed yet. A user deposits one or more listed tokens in one call, each priced by its feed, and receives BASK; redeem burns BASK for a pro-rata share of every held token, paid at once while at most directLimit (50) assets are held, otherwise booked as owed and collected with claim(tokens[], to). A deposit also needs, for every deposited and every held unretired token, its Uniswap v3 pool's 30-minute mean price (in quote tokens times the quote feed, with a mean-liquidity floor) within 3% of its feed; a token with no pool needs a feed under 26 hours old instead. The pool only blocks; it never sets the price. One owner and one guardian; owner changes are proposals that wait 2 days, then only the owner executes them, and they lapse 7 days later; the guardian can cancel any except its own replacement. Settings change only by proposal within fixed bounds. Trusted: the owner pairs each token with its true feed, pool and quote feed. The issuer can pause, block, burn or upgrade the Stock Tokens; feeds update 24/5 (stop Friday 20:00 New York, restart Sunday 20:00, pause on US holidays). This is the sixth build, written fresh from the text; the fifth was audited. Changes since: deposits open only inside hours counted in seconds since Sunday 00:00 New York time, starting 72000-504000 (Sunday 20:00 to Friday 20:00), with US daylight saving computed in code (dst 0 = US rule, second Sunday of March to first Sunday of November at 02:00 local; 1 = never; 2 = always); freshCount 1 and freshHours 1 from deployment (one listed unretired feed must have updated within the hour, so holidays close); size rule 24,576 bytes.
>
> Look hardest at:
>
> 1. Redeem and claim can never be blocked or made to revert: not by the owner, the guardian, any in-bounds setting or combination (balanceGas, payGas, directLimit, maxAssets, hours, dst, freshCount), a paused, blacklisted, reverting, gas-burning, lying or upgraded token, a stale or wrong feed, pool or quote feed, retirement or removal; they work at weekends, on holidays and outside hours. With maxAssets assets in any state a redeem stays under 28,000,000 gas on both paths. Check managedAssetCount and the managed bitmap (removeRetired's swap-and-pop, the all-held shortcut), the vault-only pay function and owed/totalOwed.
>
> 2. NewYorkTime and insideHours: civil-date and weekday math, the two daylight change instants, weekSecond for dst 0/1/2, the window [from, to), 0-0 = always open, the Hours and Dst bounds. Is there any instant from Friday 20:00 to Sunday 20:00 New York (both seasons, change weekends) when a deposit passes with the start values, or a weekday instant inside the window refused for hours? Freshness: only main feeds of unretired listed assets count (never a retired asset's or a quote feed); the edges.
>
> 3. The pool check in PoolOracle and TickMath: token0/token1 orientation, 6-decimal USDG and 18-decimal WETH quotes, harmonic-mean liquidity against minLiquidity, poolGas, overflow and rounding. Does a set pool that fails, is drained or is under its floor always block, never fall back? Can a pool, quote feed or feed make deposit, depositStatus, previewDeposit or allAssets revert instead of returning a reason, or change the shares minted?
>
> 4. Nobody moves assets out except redeem and claim paying the user; nobody mints BASK except deposit (plus fee shares and the 1e15 dead shares). No fee while feeRecipient is unset; once set, exactly 0.5% in and out. Look at every proposal kind, execute, Resync (owed tokens into managed? abuse on a retired asset?), removeRetired, close, flagDeficit, recognizeLoss and reentrancy.
>
> 5. Proposals: can anyone but the owner execute; skip the 2 days; escape the guardian's cancel; can a voided, expired or stale proposal execute after a retire, a removal and relisting, a later close (Reopen) or a NAV cap lowering; can a retired asset take any proposal but Resync; can a setting leave its bounds or break the gas rules (maxAssets x (balanceGas + 60,000) and directLimit x (balanceGas + payGas + 70,000) at most 28,000,000); can the guardian become owner.
>
> 6. Deposit share math: rounding, first-deposit and donation attacks, managed versus balance, a deposited token's balance covering totalOwed, retired assets out of NAV and every deposit check, flagDeficit and recognizeLoss after a burn or recovery, and the inline assembly under via_ir (BoundedCall, balance read, Transfer log, the self-call).
>
> 7. Anything the code does that the text does not say, or the text says and the code does not do.
>
> Accepted by the owner, report only if worse: deposits closed Friday 20:00 to Sunday 20:00 New York and whenever no listed feed updated in the last hour; profit from feed lag within the 3% deviation, including the seconds after the Sunday reopen when one feed has posted and others show Friday's answer; no 3% bound for a no-pool asset under 26 hours; no per-asset limit; anyone can stop deposits by moving a thin pool; a held asset whose pool fails stops deposits until it recovers or a Pool proposal executes; depositors after a retire share its tokens; no fee while unset; issuer-credited tokens stay outside managed until a Resync; hasPause is decided at listing; every unretired balance is read on each deposit; views read during a token callback can be inconsistent; an unreadable balance during a shortfall books the leg from managed, claims first come first served; a larger shortfall restarts the 7-day clock; a complete loss leaves NAV 0; the ownership handover takes effect at once; a token debiting more than the amount strands claims; a retired asset's dust keeps its slot and counts toward directLimit; one wei in more than directLimit assets books every redemption; a receiver that cannot call claim cannot collect; minimums are positional; BASK sent to the vault is lost; an absurd quote feed answer makes pricing revert; close to 250 held assets may not fit one deposit. Operating rules: pause deposits before a Resync, never before the first deposit; pool cardinality above poolWindow, poolGas 150,000; fund a replacement before retiring the last held stock; flagDeficit after a recovery; pause deposits when any asset is short; never list a weekend-posting feed while freshCount is 1.

| | |
|---|---|
| Repository | https://github.com/identity-md-launches/launch-1110-basket.git |
| Commit | `50acd7248c2ce59907a963a648115900d629f352` |
| Job | `7040f8d5-6711-4a2a-ba54-3b09c2cdf728` |
| Judged | 2026-10-09 02:10 UTC |
| Findings | 3 low · 3 info |

Four agents audited the code as it is at `50acd72`, 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. Low: One failed token leg reverts the whole claim batch, rolling back healthy payments

`src/BaskVault.sol:762`

```
                    if (!_tryPay(token, to, amount, gasleft())) revert PaymentFailed(token);
```

redeem isolates every leg (an unreadable balance falls back to managed and a failed pay self-call becomes debt), but claim does not: its balance read at line 757 reverts with BalanceUnreadable and this line reverts with PaymentFailed, so one paused, blacklisted, reverting or gas-burning token in claimTokens[] rolls back every earlier payment in the same transaction and prevents the later ones. The brief asks that claim never be made to revert by such a token. No debt is lost and the caller can retry omitting the failed token (README documents this), so this is a batch-liveness defect rather than a loss; it is reported at low severity for that reason. Merged from the audit_flow and audit_permissions findings, which describe the same mechanism and fix. Fix: treat a failed balance read or a failed pay as 'leave this leg owed and continue' (restore owed/totalOwed for that leg, emit Claimed with 0), keeping caller-supplied gas so owner-set budgets cannot block claims.

**Reproduction**

Monday 2026-09-21 12:00 UTC (1789992000). Owner lists three 18-decimal tokens with fresh $1 feeds, finalizes genesis, executes Hours=(0,0) and DirectLimit=0 after 2 days. Alice deposits 1e18 of each (totalSupply 3e18 incl. dead shares) and redeems 1.5e18 BASK to Bob; Bob is owed 0.5e18 of each token. Token B's transfer is then made to revert. Bob calls claim([A,B], Bob). Expected: A pays 0.5e18, B stays owed, call succeeds. Actual: the call reverts with PaymentFailed(B); Bob receives 0 of A and both debts remain 0.5e18. claim([A]) alone succeeds. Verified with `forge test --match-path test/scratch/ClaimBatchReview.t.sol -vv` (the attached proof), which fails at 'a blocked leg must not revert the whole claim' after asserting the revert data is PaymentFailed(B). test/BasketAdversarial.t.sol testClaimBatchFailureRevertsEarlierPaymentsAndDuplicateClaimsPayOnce pins the same rollback.

**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";

contract ReviewFeed {
    uint8 public constant decimals = 8;
    function latestRoundData() external view returns (uint80,int256,uint256,uint256,uint80) {
        return (1, 1e8, block.timestamp, block.timestamp, 1);
    }
}
contract ReviewToken {
    uint8 public constant decimals = 18;
    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowance;
    uint256 public mode;
    bool public blocked;
    function setMode(uint256 m) external { mode = m; }
    function setBlocked(bool b) external { blocked = b; }
    function mint(address a, uint256 n) external { balances[a] += n; }
    function approve(address a,uint256 n) external returns(bool) { allowance[msg.sender][a] = n; return true; }
    function balanceOf(address a) external view returns(uint256) {
        uint256 b = balances[a];
        uint256 m = mode;
        if (m == 1) { assembly ("memory-safe") { invalid() } }
        if (m == 2) {
            assembly ("memory-safe") {
                mstore(0, b)
                for {} gt(gas(), 40) {} {}
                return(0, 32)
            }
        }
        return b;
    }
    function transferFrom(address a,address b,uint256 n) external returns(bool) {
        allowance[a][msg.sender] -= n; balances[a] -= n; balances[b] += n; return true;
    }
    function transfer(address a,uint256 n) external returns(bool) {
        require(!blocked, "blocked"); balances[msg.sender] -= n; balances[a] += n; return true;
    }
}
contract ClaimBatchReviewTest is Test {
    BaskVault v;
    ReviewToken[] stocks;
    address constant ALICE = address(0x1234567890123456789012345678901234567890);
    address constant BOB = address(0x2345678901234567890123456789012345678901);
    function setUp() public { vm.warp(1789992000); v = new BaskVault(address(this), address(0x100)); }
    function change(BaskVault.Setting s,uint256 x) internal {
        BaskVault.Action memory a; a.kind = BaskVault.Kind.Setting; a.setting = s; a.value = x;
        uint256 id = v.propose(a); vm.warp(vm.getBlockTimestamp()+2 days); v.execute(id);
    }
    function populate(uint256 count) internal {
        change(BaskVault.Setting.Hours,0);
        address[] memory ts = new address[](count); uint256[] memory ns = new uint256[](count);
        for(uint256 i; i<count; ++i) {
            ReviewToken t = new ReviewToken(); stocks.push(t); ts[i] = address(t); ns[i] = 1e18;
            v.listGenesis(address(t), address(new ReviewFeed()), address(0), address(0), 0);
            t.mint(ALICE, 1e18); vm.prank(ALICE); t.approve(address(v),1e18);
        }
        v.finalizeGenesis(); vm.prank(ALICE); v.deposit(ts,ns,ALICE,0,vm.getBlockTimestamp());
    }
    function testClaimSkipsBlockedLegAndPaysHealthyLeg() public {
        change(BaskVault.Setting.DirectLimit,0); populate(3);
        vm.prank(ALICE); v.redeem(1.5e18,BOB,new uint256[](0),vm.getBlockTimestamp());
        stocks[1].setBlocked(true);
        address[] memory ts = new address[](2); ts[0] = address(stocks[0]); ts[1] = address(stocks[1]);
        vm.prank(BOB);
        (bool ok, bytes memory failure) = address(v).call(abi.encodeCall(v.claim,(ts,BOB)));
        if (!ok) assertEq(failure,abi.encodeWithSelector(BaskVault.PaymentFailed.selector,ts[1]));
        assertTrue(ok, "a blocked leg must not revert the whole claim");
        assertEq(stocks[0].balances(BOB),0.5e18);
        assertEq(v.owed(BOB,ts[1]),0.5e18);
    }
}
```

### 2. Low: A pool configured with minLiquidity 0 lets a fully drained pool approve deposits instead of blocking

`src/BaskVault.sol:881`

```
            if (!ok || liq < a.minLiquidity) return (Reason.Pool, answer, updatedAt, 0);
```

_poolConfig accepts a nonzero pool with minLiquidity = 0 (listGenesis, List and Pool proposals), and the only liquidity test is liq < minLiquidity. A Uniswap v3 pool with zero in-range liquidity for the whole window still answers observe(): its secondsPerLiquidity accumulator divides by max(liquidity, 1), so PoolOracle.consult returns ok = true with harmonic-mean liquidity floor(window*(2^160-1)/((window<<128)<<32)) = 0. 0 < 0 is false, so the drained pool passes and the comparison proceeds with its last tick, which anyone can move at no cost in an empty pool. The brief requires a set pool that is drained to always block, never fall back; here it approves, and with a stale main feed older than noPoolAge (but within maxAge) it even bypasses the no-pool age rule because the pool branch is taken. It requires the owner to have chosen a zero floor, which is why this is low rather than medium; but 0 is the natural 'disabled' value and the contract accepts it silently. Merged from the audit_flow, audit_economics and audit_permissions findings (same mechanism, same fix). Fix: in _price return Reason.Pool when liq == 0 regardless of the floor, or reject minLiquidity == 0 for a nonzero pool in _poolConfig.

**Reproduction**

Monday 2026-09-21 12:00 UTC (1789992000). List an 18-decimal stock with an 8-decimal main feed answering 100e8 updated 64 hours earlier (inside maxAge 80h, beyond noPoolAge 26h), attach a pool (stock = token0, 18-decimal quote token) with minLiquidity 0 and a fresh 8-decimal quote feed answering 100e8; list two more assets with fresh feeds and finalize genesis. The pool's observe([1800,0]) returns tickCumulatives [0,0] and secondsPerLiquidityCumulativeX128 [0, 1800<<128] (exact v3 output for a pool held at tick 0 with zero liquidity throughout). Expected: depositStatus([stock]) = Reason.Pool (12) and deposit([stock],[10e18],...) reverts DepositUnavailable(Pool, stock). Actual: depositStatus returns Reason.None (0) and the deposit succeeds, minting 1000e18-1e15 shares. Verified with `forge test --match-path test/scratch/DrainedPoolReview2.t.sol -vv` (attached proof): fails '0 != 12' and 'next call did not revert as expected'. The audit_flow variant with a 6-decimal quote token reproduces the same way.

**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";
import {PoolOracle} from "src/libraries/PoolOracle.sol";

contract ReviewToken {
    uint8 public constant decimals = 18;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    function mint(address to, uint256 amount) external { balanceOf[to] += amount; }
    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;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        return true;
    }
    function transfer(address to, uint256 amount) external returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}

contract ReviewFeed {
    uint8 public constant decimals = 8;
    uint256 public updatedAt;
    constructor(uint256 at) { updatedAt = at; }
    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
        return (1, 100e8, updatedAt, updatedAt, 1);
    }
}

// Exact observe deltas of a v3 pool held at tick 0 and zero active liquidity
// throughout the requested interval. V3 divides seconds by max(liquidity, 1).
contract ReviewDrainedPool {
    address public token0;
    address public token1;
    constructor(address a, address b) { token0 = a; token1 = b; }
    function observe(uint32[] calldata ago) external pure returns (int56[] memory ticks, uint160[] memory secondsPerLiquidity) {
        ticks = new int56[](2);
        secondsPerLiquidity = new uint160[](2);
        secondsPerLiquidity[1] = uint160(ago[0]) << 128;
    }
}

contract DrainedPoolReviewTest is Test {
    BaskVault private vault;
    ReviewToken private stock;
    ReviewDrainedPool private pool;

    function setUp() public {
        vm.warp(1789992000); // Monday 2026-09-21 12:00 UTC, inside default hours.
        vault = new BaskVault(address(this), address(0x1234));
        stock = new ReviewToken();
        ReviewToken quote = new ReviewToken();
        pool = new ReviewDrainedPool(address(stock), address(quote));
        // Main feed is beyond the no-pool 26-hour limit but inside maxAge.
        ReviewFeed staleMain = new ReviewFeed(block.timestamp - 64 hours);
        ReviewFeed quoteFeed = new ReviewFeed(block.timestamp);
        vault.listGenesis(address(stock), address(staleMain), address(pool), address(quoteFeed), 0);
        for (uint256 i; i < 2; ++i) {
            ReviewToken other = new ReviewToken();
            ReviewFeed freshMain = new ReviewFeed(block.timestamp);
            vault.listGenesis(address(other), address(freshMain), address(0), address(0), 0);
        }
        vault.finalizeGenesis();
        stock.mint(address(this), 10e18);
        stock.approve(address(vault), 10e18);
    }

    function testDrainedPoolMustReturnPoolReason() public view {
        (bool ok, int24 tick, uint128 liquidity) = PoolOracle.consult(address(pool), 1800, 150000);
        assertTrue(ok);
        assertEq(tick, 0);
        assertEq(liquidity, 0);
        address[] memory ts = new address[](1);
        ts[0] = address(stock);
        (BaskVault.Reason reason,) = vault.depositStatus(ts);
        assertEq(uint256(reason), uint256(BaskVault.Reason.Pool), "drained pool must block deposits");
    }

    function testDrainedPoolMustRejectDeposit() public {
        address[] memory ts = new address[](1);
        ts[0] = address(stock);
        uint256[] memory amounts = new uint256[](1);
        amounts[0] = 10e18;
        vm.expectRevert(abi.encodeWithSelector(BaskVault.DepositUnavailable.selector, BaskVault.Reason.Pool, address(stock)));
        vault.deposit(ts, amounts, address(this), 0, block.timestamp);
    }
}
```

### 3. Low: redeem accepts the vault as receiver; the leg is booked as debt nobody can claim and the tokens leave accounting forever

`src/BaskVault.sol:688`

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

deposit rejects receiver == address(this) (line 625) but redeem rejects only address(0). With the vault as receiver each direct leg runs pay(token, address(this), leg): a standard transfer to self leaves the balance unchanged, the exact-debit check fails, the self-call reverts and the leg is recorded as owed[address(this)][token] with totalOwed[token] increased, after managed[token] was already reduced. No account can call claim as the vault, so totalOwed[token] can never decrease again; _available subtracts it in every later redemption, deposit health check and Resync, so the tokens sit in the vault permanently excluded from all accounting, and the redeemer's shares were burned for nothing. The same stranded totalOwed also makes removeRetired(token) impossible forever, since it requires totalOwed == 0. Self-inflicted for an EOA, but routers and UIs that default the receiver to the contract being called lose the whole redemption. Fix: add `|| receiver == address(this)` here (and the same for claim's `to`).

**Reproduction**

Monday 2026-09-21 12:00 UTC, three genesis assets, hours left at defaults. Alice deposits 100e18 of stock[0] at $100 (8-decimal feed) and holds 1e22-1e15 BASK. Alice calls redeem(shares/2, address(vault), [], now). Expected: revert InvalidAddress, as deposit does. Actual: the call succeeds; legs[0] = 49999995000000000000, owed[vault][stock0] = 49999995000000000000, totalOwed[stock0] = same, managed[stock0] = 50000005000000000000 while the vault's balance is still 100e18. A Resync proposal executed afterwards leaves managed unchanged because available = balance - totalOwed = managed. Verified with `forge test --match-path test/scratch/RedeemToVaultReview.t.sol -vv` (attached proof, fails '49999995000000000000 != 0') and test/scratch/JudgeProbe.t.sol testRedeemToVaultStrandsLeg including the Resync step.

**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";

contract RFeed {
    uint8 public constant decimals = 8;
    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
        return (1, 100e8, block.timestamp, block.timestamp, 1);
    }
}

contract RToken {
    uint8 public constant decimals = 18;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    function mint(address a, uint256 n) external { balanceOf[a] += n; }
    function approve(address a, uint256 n) external returns (bool) { allowance[msg.sender][a] = n; return true; }
    function transferFrom(address a, address b, uint256 n) external returns (bool) {
        allowance[a][msg.sender] -= n; balanceOf[a] -= n; balanceOf[b] += n; return true;
    }
    function transfer(address a, uint256 n) external returns (bool) {
        balanceOf[msg.sender] -= n; balanceOf[a] += n; return true;
    }
}

/// redeem accepts receiver == address(this); deposit does not. The leg is booked as
/// owed[vault][token] that no account can ever claim, so totalOwed[token] never falls
/// again and the tokens leave both managed and available for good.
contract RedeemToVaultReviewTest is Test {
    BaskVault v;
    RToken stock;
    address alice = address(0xA11CE);

    function setUp() public {
        vm.warp(1789992000); // Monday 2026-09-21 12:00 UTC, inside default hours.
        v = new BaskVault(address(this), address(0x100));
        for (uint256 i; i < 3; ++i) {
            RToken t = new RToken();
            if (i == 0) stock = t;
            v.listGenesis(address(t), address(new RFeed()), address(0), address(0), 0);
        }
        v.finalizeGenesis();
        stock.mint(alice, 100e18);
        vm.prank(alice);
        stock.approve(address(v), type(uint256).max);
        address[] memory ts = new address[](1); ts[0] = address(stock);
        uint256[] memory ns = new uint256[](1); ns[0] = 100e18;
        vm.prank(alice);
        v.deposit(ts, ns, alice, 0, vm.getBlockTimestamp());
    }

    function testRedeemRejectsVaultAsReceiver() public {
        uint256 shares = v.balanceOf(alice) / 2;
        vm.prank(alice);
        (bool ok, bytes memory ret) = address(v).call(
            abi.encodeCall(v.redeem, (shares, address(v), new uint256[](0), vm.getBlockTimestamp()))
        );
        if (ok) {
            // Document the actual damage before failing: the leg is owed to the vault itself.
            uint256 stranded = v.owed(address(v), address(stock));
            emit log_named_uint("stranded owed[vault][stock]", stranded);
            emit log_named_uint("totalOwed[stock]", v.totalOwed(address(stock)));
            emit log_named_uint("managed[stock]", v.managed(address(stock)));
            emit log_named_uint("vault balance", stock.balanceOf(address(v)));
            assertEq(stranded, 0, "redeem to the vault must not book a claim nobody can collect");
        }
        assertFalse(ok, "redeem must reject receiver == address(this) like deposit does");
        assertEq(ret, abi.encodeWithSelector(BaskVault.InvalidAddress.selector));
    }
}
```

### 4. Info: transferFrom(address(0), to, 0) succeeds and emits a mint-shaped Transfer(0, to, 0)

`src/BaskVault.sol:283`

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

_transfer validates only `to`. transferFrom with from == address(0) and amount == 0 passes the allowance branch (0 is not < 0), writes allowance[0][caller] = 0 with an Approval(0, caller, 0) event, and _update takes the from == address(0) mint branch, logging Transfer(address(0), to, 0). No supply is created (amount is forced to 0 because allowance[0][x] is always 0), but any account can emit unlimited zero-value mint-shaped events, which indexers treat as mints. OpenZeppelin ERC20 reverts ERC20InvalidSender here. Fix: revert in _transfer when from == address(0).

**Reproduction**

Any account calls vault.transferFrom(address(0), alice, 0). Expected: revert. Actual: returns true and emits Approval(address(0), caller, 0) and Transfer(address(0), alice, 0). Verified with test/scratch/JudgeProbe.t.sol testTransferFromZeroEmitsMint (vm.expectEmit on Transfer(0, alice, 0) passes).

### 5. Info: No-pool feed age is inclusive: exactly 26 hours passes while the text says under 26 hours

`src/BaskVault.sol:876`

```
            if (block.timestamp - updatedAt > cfg.noPoolAge) reason = Reason.NoPoolAge;
```

The brief says a token with no pool needs a feed under 26 hours old. The check rejects only strictly older than noPoolAge, so updatedAt == block.timestamp - 26 hours is accepted; test/Oracle.t.sol pins this inclusive behaviour. The same inclusive edge applies to maxAge (line 855) and freshHours (line 944). One-second text/code discrepancy; change `>` to `>=` if the text is authoritative, otherwise document the inclusive bound.

**Reproduction**

Genesis asset without pool, defaults; set its feed updatedAt = block.timestamp - 26 hours. depositStatus([token]) returns Reason.None and a deposit succeeds. At block.timestamp - 26 hours - 1 it returns Reason.NoPoolAge. Expected per text: NoPoolAge at exactly 26 hours.

### 6. Info: lowerNAVCap changes a setting immediately and without a floor, outside the proposal path the text describes

`src/BaskVault.sol:345`

```
        if (cap >= NAV_CAP) revert InvalidInput();
```

The text states settings change only by proposal within fixed bounds. lowerNAVCap is immediate, owner-only, has no lower bound and no timelock: cap = 0 makes every deposit and previewDeposit revert CapExceeded at once (nav > 0 after the first deposit) and voids every pending RaiseCap via capEpoch; raising back needs a 2-day proposal. Redeem and claim are unaffected, so in effect it equals pauseDeposits plus a 2-day recovery delay, and README documents it. Recorded as a text-versus-code difference, not a vulnerability. If proposal-only is intended, route lowering through a proposal kind or add a floor such as the current NAV.

**Reproduction**

After a first deposit (nav > 0) the owner calls lowerNAVCap(0) in one transaction. Immediately deposit of any amount reverts CapExceeded, previewDeposit reverts CapExceeded, and proposal(id) for a pending RaiseCap reports pending == false. Reopening requires propose(RaiseCap) and 2 days.

---

Judge's submission `d76c8811bc9ae223de9eeafcefa8ebf21da01b0bd37c46bc306160a98421703e`, 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.
