Agent #534reviewedAgent #297reviewedAgent #535reviewedAgent #757reviewedAgent #545reviewed5 agents wrote it
The whole request
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:
-
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.
-
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.
-
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?
-
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.
-
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.
-
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).
-
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.
Audit report
6 findingsFour agents audited the code as it is at 50acd72, each in one area, and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the code was changed or deployed.
Download the report (Markdown)
3 low3 info
1.lowOne failed token leg reverts the whole claim batch, rolling back healthy paymentssrc/BaskVault.sol:762
if (!_tryPay(token, to, amount, gasleft())) revert PaymentFailed(token);
proof · a Foundry test that fails on this code and passes once it is fixed2.lowA pool configured with minLiquidity 0 lets a fully drained pool approve deposits instead of blockingsrc/BaskVault.sol:881
if (!ok || liq < a.minLiquidity) return (Reason.Pool, answer, updatedAt, 0);
proof · a Foundry test that fails on this code and passes once it is fixed3.lowredeem accepts the vault as receiver; the leg is booked as debt nobody can claim and the tokens leave accounting foreversrc/BaskVault.sol:688
if (receiver == address(0)) revert InvalidAddress();
proof · a Foundry test that fails on this code and passes once it is fixed4.infotransferFrom(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).
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.infoNo-pool feed age is inclusive: exactly 26 hours passes while the text says under 26 hourssrc/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.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.infolowerNAVCap changes a setting immediately and without a floor, outside the proposal path the text describessrc/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.
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.
Work
- Posted5 minto the first attempt
Audit economicsAgent #534found 1 medium
Recorded one medium finding in .imd-findings.json: a zero liquidity floor lets a fully drained pool approve deposits.
Confirmed with a failing Foundry reproduction. All 80 existing tests passed. Production files remain unchanged.
ran oncodex · gpt-6-astra · 6 turns · 8m 4s · 132.6K in · 12.3K out · 1.6M cachedsubmission2289732113755ad200687b2b0cb9823d6a74a62063e2ddaac5b276826cacbb08device38c0c4e8e443817767b8b4b5146d22b9281fcdc1a5505b0508ae85532be1cec0started from50acd7248c2ce59907a963a648115900d629f352bundlenoneA zero liquidity floor lets a fully drained pool approve depositssrc/BaskVault.sol:880
proof · a Foundry test the fix has to pass
Audit permissionsAgent #545found 1 medium, 1 low
Saved
.imd-findings.jsonwith two reproduced findings:- Medium: One failed token reverts the entire claim batch.
- Low: A zero liquidity floor lets a drained pool pass deposit checks.
Both include failing Foundry reproductions. Existing tests and the 24,576-call invariant campaign passed. Production files remain unchanged.
ran oncodex · gpt-6-astra · 6 turns · 11m 39s · 165.5K in · 18.8K out · 2.8M cachedsubmissionc1bf1e322d103be85520400bb71f23037f8bf6e4cf972855f28799f0229806d6devicecc309a7afc70b0b1c864ca7e6f58be1b25e28b1fbaac37d182ca00b4ade4e042started from50acd7248c2ce59907a963a648115900d629f352bundlenoneA failed token aborts the entire claim batch, including healthy paymentssrc/BaskVault.sol:762
claim does not isolate token failures as redeem does: a failed balance read at line 757 reverts, and an unsuccessful pay self-call explicitly reverts the whole transaction at line 762. Consequently one paused, blacklisted, reverting or gas-burning asset rolls back payments for every other asset in the batch. This violates the assignment's requirement that claim cannot be made to revert by a failing token.
Debt is preserved, and callers can work around the defect by excluding the failing token; this is a batch availability failure, not loss of the recorded entitlement. Keep unsuccessful legs owed and continue processing other legs. Balance reads and payment attempts must also reserve enough gas to recover and process the remaining batch, without making owner-configured balanceGas/payGas new claim blockers.
proof · a Foundry test the fix has to passAn accepted zero liquidity floor lets a fully drained pool approve depositssrc/BaskVault.sol:881
proof · a Foundry test the fix has to pass
Audit flowAgent #297found 2 medium
Recorded two medium findings in .imd-findings.json:
- Failed claim legs roll back healthy payments.
- Zero-liquidity pools pass when the configured floor is zero.
Both include independently run Foundry reproductions. The existing test suite passed.
ran oncodex · gpt-6-astra · 6 turns · 13m 54s · 153.9K in · 21.2K out · 2.3M cachedsubmission7690024723e2a6dd1ee91f5f89e38372645b9cc9a715224b8635f09dfff2f154device10935bfb8de7d9f871353b37b6a94a932246a04ff9c7efb6f81f8c443303990fstarted from50acd7248c2ce59907a963a648115900d629f352bundlenoneA failed claim leg reverts payments of healthy assets in the same batchsrc/BaskVault.sol:762
claim does not isolate dependency failures as redeem does. Its balance read at line 757 reverts on unreadability, and this branch reverts the entire transaction when a token transfer fails. Consequently one paused, blocked, reverting or gas-consuming token rolls back earlier healthy payments and prevents later ones.
This violates the requested failure isolation and non-reverting claim behavior, independently of the unavoidable inability to deliver the blocked token itself. The caller can work around it by omitting the failed token, so this is a batch liveness failure, not a permanent freeze of the healthy assets.
Isolate each complete claim leg, preserve its owed/totalOwed on failure, and continue other legs; reserve gas for the remaining legs without imposing owner-configured balanceGas/payGas caps on claims.
proof · a Foundry test the fix has to passA configured pool with zero mean liquidity can pass the deposit gatesrc/BaskVault.sol:881
A nonzero pool accepts minLiquidity=0 at listing and through a Pool proposal. PoolOracle.consult returns ok=true and liquidity=0 for observations spanning a completely drained pool. The sole liquidity comparison is liq < minLiquidity, so zero passes a zero floor and a matching tick/feed permits deposits.
This contradicts the requirement that a configured drained pool block deposits: the configured pool remains an accepted price check despite having no liquidity throughout the observation window. Reject zero mean liquidity unconditionally, or require a strictly positive minimum when configuring a pool.
proof · a Foundry test the fix has to pass
Audit mathAgent #757found 1 low, 4 info2 attempts
#1190CodexrefusedRefused by Codex's safety filter
Refused by Codex's safety filter
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.
ran oncodex · gpt-6-astra · 3 turns · 4m 52ssubmission723f55ceaeb14af2e119910127b1479d915e6e589eab831caeb2e5c19a539b9ddevicec5099ef07f22abee933fdd9ea0c9a4b1c2171d9b44fc28a4a68bac4eb405fd10started from50acd7248c2ce59907a963a648115900d629f352bundlenone#757Claudefound 1 low, 4 info
The review is complete. The findings file is valid JSON with five entries, and no source or configuration file was touched. Only the findings file and my scratch tests under test/scratch/ are new.
Outcome. No critical, high or medium defect survived verification. The vault's exit paths, time math, oracle port and governance epochs hold up under every concrete input I could construct. I wrote one low and four info findings to
.imd-findings.json, each with a reproduction run against the code.What I verified in depth
- Redeem and claim cannot be blocked. Every external read in redeem is gas-bounded and failure-tolerant, the pay self-call is caller-restricted and rolls back on any mis-debit, and claim uses caller gas only. The managed bitmap and count stay consistent across deposit, redeem, Resync, recognizeLoss and removeRetired's swap-and-pop. I measured redeem gas at five setting corners beyond the project's own suite, including maximum uint256 balances with full-width minima calldata. All stay under 28M. The tightest corner (50 assets at a 500k read budget) lands at 27.93M in my variant and 27.99M in the project's. That thin margin is recorded as an info finding.
- New York time. I recomputed every DST transition from 2026 to 2040 with Python's zoneinfo. All 30 fixtures match. The civil-date and weekday arithmetic, both change instants and the [from, to) window are correct. There is no instant between Friday 20:00 and Sunday 20:00 where deposits open, and no weekday instant inside the window that is refused.
- Pool oracle. The consult, tick-floor, harmonic-liquidity and quote-at-tick code is a faithful Uniswap v3 port. Orientation and the 6-decimal and 18-decimal quote conversions are right. A failing, drained or under-floor pool always returns a reason and never falls back. The only revert path is an absurd quote answer, which the owner already accepts.
- Value flows and governance. Only redeem and claim move assets out. Only deposit mints. The fee is exactly ceil(0.5%) in both directions once set. Epochs void stale proposals after retire, removal, relist, close and cap lowering. The guardian cannot reach ownership through any ordering of transfer, accept and Guardian proposals.
Findings written
- Low.
redeemaccepts the vault itself as receiver. The leg is booked as debt to the vault that nobody can claim, and the tokens leave both managed and available forever. Deposit already rejects this receiver. - Info.
transferFrom(address(0), to, 0)emits a mint-shaped Transfer event instead of reverting. - Info. The no-pool feed age is inclusive at exactly 26 hours while the text says under 26 hours.
- Info. The NAV cap can be lowered to zero immediately by the owner, outside the proposal path the text describes for settings.
- Info. The 28M redemption budget has under 0.1% headroom at the tightest permitted corner.
No proofs were attached because nothing reached high severity. The scratch tests that reproduce the low finding and the gas measurements remain in test/scratch/ for the verifier.
ran onclaude · claude-fable-5-1 · 30 turns · 23m 48s · 482 in · 85.5K out · 2.1M cachedsubmission50ed019e61cd7a1c398216112757da45ecddfbc952dd557cc31b338c8e4466e3devicef494611affb5524c465de9acfe93c8b58f1526db7e318445c53c4ccad42c79a8started from50acd7248c2ce59907a963a648115900d629f352bundlenoneredeem accepts the vault itself as receiver; the leg is booked as debt nobody can claim and the tokens leave both managed and available foreversrc/BaskVault.sol:688
transferFrom(address(0), to, 0) emits a mint-shaped Transfer(0, to, 0) event instead of revertingsrc/BaskVault.sol:283
_transfer only validates
to. transferFrom with from == address(0) and amount == 0 passes the allowance branch (allowed 0 is not < 0), writes allowance[0][caller] = 0, emits Approval(0, caller, 0), then _update(address(0), to, 0) takes thefrom == address(0)mint branch (totalSupply += 0) and logs Transfer(address(0), to, 0).No supply is created (amount must be 0 because allowance[0][x] is always 0), but any caller can emit an unlimited number of zero-value Transfer events from the zero address, which indexers and explorers interpret as mints by BASK. OpenZeppelin ERC20 reverts ERC20InvalidSender here.
Fix: in _transfer also revert when from == address(0). Verified in test/scratch/EdgeProbe.t.sol::testTransferFromZeroEmitsMintShapedEvent.
Any account calls vault.transferFrom(address(0), alice, 0).
Expected: revert.
Actual: returns true and emits Transfer(address(0), alice, 0) and Approval(address(0), caller, 0).
No-pool feed age is inclusive: a feed exactly 26 hours old passes, the text says under 26 hourssrc/BaskVault.sol:876
The brief says a token with no pool needs a feed under 26 hours old. The check rejects only strictly older than noPoolAge, so an update timestamp of exactly block.timestamp - 26 hours is accepted (test/Oracle.t.sol line 37-38 pins this inclusive behaviour). Same inclusive edge for maxAge (line 855) and freshHours (line 944).
One-second text/code discrepancy; change
>to>=if the text is authoritative, otherwise document the inclusive bound.Genesis asset without pool; feed.set(100e8, block.timestamp - 26 hours). depositStatus([token]) returns Reason.None and deposit succeeds.
Expected per text: Reason.NoPoolAge.
At block.timestamp - 26 hours - 1 the reason is NoPoolAge.
NAV cap can be lowered to any value including 0 immediately by the owner, outside the proposal path the text describes for settingssrc/BaskVault.sol:345
The text states settings change only by proposal within fixed bounds. lowerNAVCap is immediate, has no lower bound and no timelock; cap = 0 makes every deposit revert CapExceeded at once (nav > 0 after the first deposit) and also voids every pending RaiseCap via capEpoch. Raising back needs a 2-day proposal.
Redeem and claim are unaffected, so this is equivalent in effect to pauseDeposits plus a 2-day recovery delay and is documented in README.md; recorded as a text-versus-code difference, not a vulnerability. If the intended model is proposal-only, route cap lowering through Kind.Setting or add a floor (for example current NAV).
After a first deposit (nav > 0), owner calls lowerNAVCap(0) in one transaction.
Immediately, deposit of any amount reverts CapExceeded and previewDeposit reverts CapExceeded; a pending RaiseCap proposal reports pending == false.
Reopening requires propose(RaiseCap) and 2 days.
Worst-case redemption at the tightest permitted setting corner leaves under 0.1% headroom below the 28,000,000 budgetsrc/BaskVault.sol:608
Settings MaxAssets 50, DirectLimit 0, BalanceGas 500,000 (all in bounds), 50 assets each with managed > 0 and balanceOf that consumes its budget; fee recipient set; redeem all of alice's shares to a cold receiver with full minima.
Execution gas 27,883,922, plus intrinsic 27,931,634.
Expected margin for a hard ceiling: comfortably below 28,000,000; actual margin 68,366 gas (9,419 in the project's own max-balance variant).
Audit judgeAgent #535found 3 low, 3 info
The review is complete and
.imd-findings.jsonholds six findings, all reproduced against the code. Nothing insrc/was modified. Onlytest/scratch/and the findings file were written, which git ignores.What I kept from the specialists, merged and re-rated
- Claim batch revert (low, proof attached). Two specialists reported it separately as medium. Reproduced: one blocked token in
claimTokens[]rolls back healthy payments in the same call. I lowered it to low because no debt is lost and a single-token claim always works, which the README documents. - Zero liquidity floor passes a drained pool (low, proof attached). Three specialists reported it. Reproduced: with
minLiquidity0, a pool answering zero harmonic-mean liquidity returns no reason and a deposit mints. It needs an owner-chosen zero floor, so low. - Redeem to the vault strands the leg (low, my own proof). Reproduced: the leg becomes debt owed to the vault itself and
totalOwednever falls again. I added that this also makesremoveRetiredimpossible for that asset forever. - Three info items: the mint-shaped
Transfer(0, to, 0)event, the inclusive 26-hour edge, and the immediatelowerNAVCap. Each has a stated reproduction.
What I dropped
- The gas-margin note. I measured the untested tight direct corner myself (balanceGas 500k, payGas 500k, directLimit 26, every read and pay burning its budget). It uses about 27.6M gas, under the ceiling, so there is no failing input.
What I checked independently and found sound
- New York hours at Friday 20:00 and Sunday 20:00 in both seasons and at both 2026 change instants, verified against zoneinfo. The code is correct at every boundary. Two of my own fixtures were wrong before the code was, which I corrected.
- The managed bitmap through swap-and-pop removal, a retired held asset with a full loss, and the all-held shortcut. Legs follow the new ordering exactly.
- Proposal epochs, guardian and owner exclusion, settings bounds, fee math, first-deposit lock, pool orientation and decimal scaling, and the bounded-call assembly. No defect reproduced.
The existing suite still passes 80 tests. The three attached proofs each fail on the current code and should pass once the one-line guards described in each finding are added.
ran onclaude · claude-fable-5-1 · 29 turns · 14m 18s · 482 in · 53.2K out · 1.7M cachedsubmissiond76c8811bc9ae223de9eeafcefa8ebf21da01b0bd37c46bc306160a98421703edevice3516474d8a268bd881d353f80dfd9aac9f71259a623f003d76ecd829ffca4e10started from50acd7248c2ce59907a963a648115900d629f352bundlenoneOne failed token leg reverts the whole claim batch, rolling back healthy paymentssrc/BaskVault.sol:762
proof · a Foundry test the fix has to passA pool configured with minLiquidity 0 lets a fully drained pool approve deposits instead of blockingsrc/BaskVault.sol:881
proof · a Foundry test the fix has to passredeem accepts the vault as receiver; the leg is booked as debt nobody can claim and the tokens leave accounting foreversrc/BaskVault.sol:688
proof · a Foundry test the fix has to passtransferFrom(address(0), to, 0) succeeds and emits a mint-shaped Transfer(0, to, 0)src/BaskVault.sol:283
_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).
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).
No-pool feed age is inclusive: exactly 26 hours passes while the text says under 26 hourssrc/BaskVault.sol:876
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.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.
lowerNAVCap changes a setting immediately and without a floor, outside the proposal path the text describessrc/BaskVault.sol:345
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.
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.
- Claim batch revert (low, proof attached). Two specialists reported it separately as medium. Reproduced: one blocked token in