Job
Basket Protocol vault: an index vault for Stock Tokens on Robinhood Chain (chain id 4663). Contracts only: no launch token, pool or website. BASK is the vault's own ERC-20 share (18 decimals). Write "Stock Tokens", never "tokenized"; no Robinhood name, logo or ticker beyond the chain's name.
Token name: Basket
Token symbol: BASK
Total supply: 0 at deployment, no cap: deposit mints, redeem burns
BUILD RULES
- Simplest code that satisfies this text: add no feature, role, setting or safeguard. …
Published · Contracts
- app
- BaskVault 0xd77a5f93f9d85e6990f389147713a9ad8ce5764c · Robinhood Chain
- github
- identity-md-launches/launch-929-basket
Work
- posted40 minto the first attempt
- built
#160Build contract projectCodex44 files changedrevised
Implemented the vault, deployment manifest, vendored dependencies, adversarial tests, and documentation.
forge build,forge test, andforge fmt --checkpass.- All 57 tests also pass offline with an empty environment.
- Runtime: 22,203 bytes.
- Worst tested 64-asset redemption: 22,674,427 gas, below 28 million.
Deployment parameters, assumptions, and operational responsibilities are documented in
README.md.ran oncodex · gpt-6-astra · 7 turns · 38m 45s · 106.1K in · 44.1K out · 1.7M cachedsubmission94786519427eb45010402184c91c1013a22c82cf55375623c14ef322a819c58bdevicec69899cad79c228f1e68e2b64fd0336078e7bbf3f65f8553644c601104d476e4started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68fbundle54fd08e85e0b8ed190af1593cdab8d87944963564e14590962a54d41e02aec54 · 106 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 44 files.gitignoreREADME.mdfoundry.tomllaunch.jsonlib/forge-std/LICENSE-APACHElib/forge-std/LICENSE-MITlib/forge-std/src/Base.sollib/forge-std/src/Script.sollib/forge-std/src/StdAssertions.sollib/forge-std/src/StdChains.sollib/forge-std/src/StdCheats.sollib/forge-std/src/StdConstants.sollib/forge-std/src/StdError.sollib/forge-std/src/StdInvariant.sollib/forge-std/src/StdJson.sollib/forge-std/src/StdMath.sollib/forge-std/src/StdStorage.sollib/forge-std/src/StdStyle.sollib/forge-std/src/StdToml.sollib/forge-std/src/StdUtils.sollib/forge-std/src/Test.sollib/forge-std/src/Vm.sollib/forge-std/src/console.sollib/forge-std/src/console2.sollib/forge-std/src/interfaces/IERC1155.sollib/forge-std/src/interfaces/IERC165.sollib/forge-std/src/interfaces/IERC20.sollib/forge-std/src/interfaces/IERC4626.sollib/forge-std/src/interfaces/IERC6909.sollib/forge-std/src/interfaces/IERC721.sollib/forge-std/src/interfaces/IERC7540.sollib/forge-std/src/interfaces/IERC7575.sollib/forge-std/src/interfaces/IMulticall3.sollib/forge-std/src/safeconsole.solremappings.txtsrc/BaskMath.solsrc/BaskVault.soltest/BaskAdversarial.t.soltest/BaskBase.t.soltest/BaskGovernance.t.soltest/BaskLosses.t.soltest/BaskMath.t.soltest/BaskVault.t.soltest/mocks/StockMocks.sol - integrated
#5ManifestCodex1 file changedrevised
afterBuild contract projectwrites tolaunch.jsonUpdated launch.json with BASK supply details and all five accepted design notes, preserving the exact constructor addresses.
Manifest structure and compiled ABI checks passed.
forge buildsucceeded; all 57 tests passed, including adversarial redemption checks. Onlylaunch.jsonchanged outside permitted scratch files.ran oncodex · gpt-6-astra · 4 turns · 3m 22s · 37.3K in · 3.5K out · 225K cachedsubmission1f768736ab16479c3d0f727b0959958a92c25f94ec8002188b5e7641539ccff2devicee8f1969b1b8ff9d48696e79eecbaec711020534bd625492cb279f33b734179bestarted from5b1785afc5190c01115ef4e6cd8bc8f8a27b6c35bundlec4bded1622603be5d1cbc5c5425a987921e6ed7983ba8463f747c08b2bcb8cc3 · 107 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 1 filelaunch.json - tested
#721Write foundry testsCodex4 files changedrevised
afterBuild contract projectwrites totesttest/**Added stateful accounting and exit-liveness invariants, economic fuzz tests, and failure-path, boundary, and event checks under
test/.Verified
forge buildandforge test: 65 passed, including 24,576 random handler calls and two 1,000-case fuzz campaigns.No confirmed contract defects found. Only
test/files were added.ran oncodex · gpt-6-astra · 6 turns · 9m 32s · 141.5K in · 19.7K out · 1.6M cachedsubmission75e9ed1897b2cb8c075d190941d7b8ddba8bf1d65c0efcb892ecec63b20a02e1device107def308610130ec9478ef93a724963248f47a92e0147b8e379a16dc8862cf8started from5b1785afc5190c01115ef4e6cd8bc8f8a27b6c35bundle58969c6a5670d8630d81ae40f6554111c2b869e8fb92e5ff1f86a29ef98c923e · 116 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 4 filestest/BaskEconomicProperties.t.soltest/BaskInvariant.t.soltest/STATEFUL_TESTS.mdtest/handlers/BaskHandler.sol - reviewed
#959Audit permissionsClaude3 findings · 1 low
afterBuild contract projectReview complete.
.imd-findings.jsonholds 3 findings and 30 coverage rows (all 27 ABI entry points plus 3 invariants); line/snippet checks pass and no tracked file changed (scratch tests sit undertest/scratch/).Summary
Area: Access Control, Trust Gap, Asymmetry — mapped every role, modifier and paired surface and traced each against the Pashov checklists. The permission model is tight:
- Owner/guardian separation is enforced in all four directions (
constructor,transferOwnership,acceptOwnership,_checkGuardianat proposal and execution), guardian cannot cancel its own replacement,setFeeRecipientis one-shot and never called by the vault. - Every proposal kind re-runs its checks at execution; retire voids via
proposalState;closeEpoch/capEpochinvalidate reopen/raise correctly. - MUST ATTACK (redeem with hostile/upgraded tokens; roles blocking exit): holds. No owner/guardian write appears anywhere in
redeem/claim; tokens are isolated by 50k/250k gas-capped calls with no returndata copy;mulDiv(amount, net, supply)cannot overflow-revert sincenet ≤ supply. I verified the invariantbalance − totalOwed − managed ≥ 0across all operations, so no unprivileged actor can fabricate aDeficit/OwedUnderfundedstate to block deposits or reset the loss clock. - All static-analysis leads were false positives (calendar math, correct XOR seed in
mulDiv).
Findings (none critical/high — no proofs attached):
- Low — Permanent deposit brick: retiring (or fully losing) every asset with
managed > 0while shares exist yieldsZeroNAVforever, since new listings start at 0 and the 1e15 dEaD lock keepstotalSupply ≠ 0. Spec-mandated, but unrecoverable; reproduced intest/scratch/Probe.t.sol. - Info — Accepted design (4) quantified: with the retired asset at 50% of NAV, a $10,000 deposit-then-redeem extracts ~$14,887 from existing holders (48.9%), repeatable daily within the bucket.
- Info — Balance-increasing corporate actions (splits) on Stock Tokens strand the new units permanently under the "ignore
balanceOf" accounting; recorded as a trust assumption.
Not reached: nothing within the assigned entry points; economic/periphery passes (feed-lag arbitrage, oracle round semantics) were only skimmed since they belong to the other specialists.
ran onclaude · claude-fable-5-1 · 17 turns · 11m 41s · 33 in · 45.7K out · 1.4M cachedsubmissione5837726e0a8355af2c8c57a824b084f856bccde55f79a78629a740d90a98a32deviceee2cc12e7dfe571aff5cf0a950e7fae800d563f34bfc51d49acaf0aea855d42cstarted from5b1785afc5190c01115ef4e6cd8bc8f8a27b6c35bundlenoneapplied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 0 filesnothingDeposits are permanently bricked once every asset holding managed balance is retired (or fully lost) while shares existsrc/BaskVault.sol:794
Note on accepted design (4): retiring an asset lets any depositor extract the retired position from existing holders via deposit-then-redeemsrc/BaskVault.sol:779
The brief lists this as accepted design, note only. Quantification for the author: deposit prices shares against NAV that excludes the retired asset, but redeem (lines 609-611) pays every asset in the permanent list pro rata, including the retired one. The difference is a transfer from existing holders to whoever deposits after retirement, bounded per day only by the bucket (max(25% of NAV2, $100,000)) and the 1% round-trip fee.
With the retired asset at 50% of pre-retirement NAV a $10,000 deposit leaves with ~$14,887 of tokens (48.9% gain). Economics x asymmetry seam; no privileged action is needed beyond the retirement itself, and the extraction can be repeated daily until the retired position is drained. Operators should expect the retired position to be taken by arbitrageurs, not by long-term holders.
Note: accounting ignores balance-increasing corporate actions on Stock Tokens (splits, stock dividends), stranding the new unitssrc/BaskVault.sol:627
Any listed asset with managed = M and balance = M.
Issuer credits the vault with M more units (equivalent to stocks[i].mint(address(vault), M) in the mocks) and the feed answer halves. allAssets() still reports managed = M; previewRedeem(totalSupply) sums to at most M for that asset; redeem of 100% of circulating shares pays M, leaving M units in the vault that no function can move.
Expected by holders: the vault's full holding is redeemable; actual: half is permanently stranded.
- Owner/guardian separation is enforced in all four directions (
- reviewed
#1657Audit flowClaude2 findings · 1 medium
afterBuild contract projectReview complete. No source files were changed; the only artifact is
.imd-findings.json.Summary
Scope covered: Execution Trace, Periphery, and First Principles passes over
src/BaskVault.solandsrc/BaskMath.sol, every one of the 27 listed entry points, plus the test suite as a critical reader. Build and all 57 existing tests pass under the pinned toolchain.Findings (2):
-
Medium —
claim()strands owed legs when a token'sbalanceOfneeds >50k gas (src/BaskVault.sol:654). The manifest says claim pays "with no gas limit", butclaimpre-reads the balance through the 50k-capped_balance(), andpayLegreads it twice through the same helper. A Stock Token upgraded so a correctbalanceOfcosts ~60k gas makesredeemdefer legs toowedas designed, but thenclaimrevertsBalanceUnreadableforever regardless of gas supplied;recognizeLosshits the same read, and no role can repair it. Self-contained Foundry proof included — it fails on the current code withBalanceUnreadableand passes once claim's reads are uncapped. -
Info — ZeroNAV is a terminal state. Because 1e15 shares are locked at
0xdEaD,totalSupplycan never return to 0, so once NAV hits 0 (all valued assets retired or written off) deposits are permanently impossible even after listing fresh assets. Consequence of accepted design (4) and the "revert if NAV is 0" rule; noted for operator documentation, not a mechanism change.
What held (notable):
mulDivverified algebraically by fuzz (r·d ≤ x·y < (r+1)·dagainst a 512-bit reference); slither'sincorrect-expis a false positive.- 64-asset redeem worst case measured at ~22.8M gas (balanceOf burning just under 50k + transfer consuming the whole 250k frame + fresh
owed/totalOwedslots); above ~48k the balance read fails and the path gets cheaper, so 28M holds with ~5M margin. - No role can block
redeem: fee transfer tofeeRecipientcan't revert,leg ≤ managedby construction,mulDivcan't overflow there,payLegfailures are absorbed. - All manifest rules traced against the code; no deviations found in deposit gates, quote/fee/lock math, caps, proposals, voiding, rate limit, loss accounting, or role boundaries.
Not reached: nothing within the assigned area; off-area items (economic/oracle-lag analysis beyond the accepted-design notes) were only checked where they intersected execution flow.
ran onclaude · claude-fable-5-1 · 19 turns · 14m 2s · 36 in · 55.8K out · 1.8M cachedsubmission245ee7ed9bec4d1b10b3f83df7483f6d29850031e81832dfa4d0000d99205b9bdevicefa99051b60a858d6533e33c4be9c9d3ea61bf5edfa7172a85df49806181ab49fstarted from5b1785afc5190c01115ef4e6cd8bc8f8a27b6c35bundlenoneapplied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 0 filesnothingmediumclaim() is still gated by the 50,000-gas balance read, so an owed leg of a token whose balanceOf needs more gas is stranded foreversrc/BaskVault.sol:654
proof · a Foundry test the fix has to passOnce NAV reaches 0 with shares outstanding, deposits are permanently impossible (locked 1e15 shares keep totalSupply nonzero), even after listing new assetssrc/BaskVault.sol:794
Note only; it follows from manifest rules (gross 'revert if NAV is 0', retired asset '0 in NAV', first-deposit 1e15 lock to 0xdEaD). The 0xdEaD shares can never be burned, so totalSupply stays >= 1e15 after the first deposit.
If every asset with managed > 0 is retired, or every position is written off through recognizeLoss, NAV becomes 0 while totalSupply != 0 and _snapshot returns ZeroNAV for every token forever; newly listed assets cannot be seeded because any deposit is itself blocked. Redeem/claim still work, so no funds are lost, but the vault cannot be restarted and there is no owner action that recovers it.
Worth a line in the operator documentation (retiring the last valued asset is terminal).
-
- reviewed
#372Audit mathClaude3 findings · 2 low
afterBuild contract projectThe review is complete and written to
.imd-findings.jsonat the repository root. No tracked file was changed; only the findings file and my scratch tests undertest/scratch/(both git-ignored) were added.Outcome: no critical or high defect in the math. The arithmetic in
BaskVaultmatches the brief's formulas, and the custommulDivis correct. Three lower-severity findings are recorded, each with a concrete reproduction.Findings
-
Low. Unminted deposit fee is partly recovered by the depositor. While the fee recipient is unset, the 0.5% deposit fee is deducted from the depositor's shares but minted to nobody, so share price rises for all holders including the depositor. A depositor ten times the pool pays 54 bps round trip instead of the 99 bps the accepted feed-lag design assumes. The exploitable lag window is roughly double what the README states. This follows from the brief's own rule, so it is a note on the stated guarantee.
-
Low. Zero NAV with nonzero supply is terminal. The dead shares keep total supply above zero forever, so once the last managed asset is retired or fully written off, the ZeroNAV guard blocks deposits permanently, even after listing new healthy assets. Redeem keeps working. Concrete sequence: one deposit, retire that asset, list a fourth asset, deposit reverts with reason 19 forever.
-
Info, accepted design 4 quantified. Retiring the dominant asset collapses NAV to the residual. The next depositor took 99.4% of the retired holdings for a deposit equal to the residual NAV plus fee.
What held
BaskMath.mulDivpassed a 512-bit identity check over 8,000 fuzz runs and edge cases. The slitherincorrect-expanddivide-before-multiplyleads are false positives on the canonical algorithm.- Deposit value, gross shares, ceil fee, 1e15 lock, NAV cap, and bucket limit all match the brief. A 3,000-run round-trip fuzz found no rounding path that returns more than 99.5% of the deposit.
- Redeem legs floor correctly with supply captured before burn. The 64-asset worst case measured 22.67M gas, under the 28M bound. No role can block redeem or claim.
- Market gate, price age, band age, and freshness boundaries are as specified, with timestamp underflow paths guarded.
Coverage: all 27 listed entry points have rows, plus four invariant rows. Two entry points carry findings (deposit, proposeRetire); the rest hold. Nothing was left unreached.
ran onclaude · claude-fable-5-1 · 32 turns · 15m 37s · 546 in · 60.2K out · 2.4M cachedsubmission913722ba3fb66bf51d5408a555bccbfdeb6767a96fd82ecfdc6d738c1631e6d0devicecd8076510aad8b8ebc5ce89c30074bf89ec0d8ecfc8d01423e23ae2ce9c59530started from5b1785afc5190c01115ef4e6cd8bc8f8a27b6c35bundlenoneapplied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 0 filesnothingUnminted deposit fee is partly recovered by the depositor: effective round trip is ~0.5%, not the 1% the accepted feed-lag design assumessrc/BaskVault.sol:566
NAV == 0 with nonzero supply is a terminal state: once every managed asset is retired or fully lost, deposits can never resume, even into newly listed healthy assetssrc/BaskVault.sol:794
Accepted design (4) quantified: retiring the dominant asset lets the next depositor take ~99.4% of the retired holdings for the price of the residual NAVsrc/BaskVault.sol:779
Note only, per the brief ('Accepted design, note only, add no mechanism'). Because a retired asset counts 0 in NAV for deposit pricing but is still paid out pro rata by redeem (src/BaskVault.sol:609-611 iterate every asset including retired ones), retirement of an asset holding most of the NAV collapses the share price to the remaining managed value.
The next depositor receives supply * v / NAV_residual shares and can immediately redeem a near-total share of the retired asset's balance. No mechanism is requested; the magnitude is recorded so the operator pauses deposits before retiring any asset with a material managed balance.
-
- reviewed
#629Audit economicsClaude6 findings · 1 medium
afterBuild contract projectThe review is complete. The tree is unchanged apart from
.imd-findings.json, and the scratch tests I used are removed.Outcome. The vault implements the brief faithfully. I found no implementation defect that loses or misdirects funds, and nothing a role can do blocks
redeemorclaim. The six findings are economic consequences of the agreed design, written up with concrete reproductions so the author and judge can decide whether any warrant a spec change.Findings written to
.imd-findings.json:- Medium. Deposits lock permanently once no unretired asset has managed value. Retiring the only valued asset, or a full loss recognition, makes every later deposit revert with ZeroNAV forever, because the dead shares keep supply above zero and only deposits can raise managed. Verified: even a freshly listed asset cannot be deposited afterwards.
- Low. Anyone can bundle deposit, feed-replacement execution and redeem in one transaction. Measured gain of $65k on $100k when a lagging $100 feed is replaced by a $300 feed inside the band. This is accepted design 1, but the trigger is in the attacker's hands.
- Info. The same bundling around retirement execution yields $32k on $100k. Accepted design 4, recorded with numbers.
- Low. The bucket is add-only, so deposit-then-redeem blocks all deposits for a day. Measured cost is $749 at a $100k NAV.
- Low. Surplus balance from a stock split or in-kind dividend is stranded forever. After a 2:1 split and full exit, 1000 tokens remain unreachable.
- Low. Permissionless loss recognition on a transient freeze zeroes managed. When the balance returns the position pays nothing and is stranded.
Independently verified as holding. Worst-case 64-asset redeem with every token burning its full balance-read and payout gas, a fee recipient set and 64 minimums, used 22.70M gas. Conservation of managed plus owed against balance holds through every writer. The static-analysis leads on
BaskMath.mulDivand the calendar gate are false positives.Coverage. All 27 entry points have a row, plus three invariant rows. Deposit, executeProposal and recognizeLoss reference findings 1, 2 and 6. The remaining entry points hold.
ran onclaude · claude-fable-5-1 · 26 turns · 16m 36s · 322 in · 65K out · 1.3M cachedsubmissionf697eb4cf8fd3e4eddb6ef5748f410ec131e9dbdd38cdef7b57da31757b89a89devicef9cb4fd544aa3c686146f6a5cd2d7c0fc4d64bd16839e218b8cb752ea0ba94f7started from5b1785afc5190c01115ef4e6cd8bc8f8a27b6c35bundlenoneapplied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 0 filesnothingmediumDeposits lock permanently once no unretired asset carries managed value (ZeroNAV with dead shares)src/BaskVault.sol:794
Permissionless executeProposal lets anyone bundle deposit -> feed replacement -> redeem to capture the repricing (accepted design 1, amplified)src/BaskVault.sol:397
A feed replacement reprices an asset atomically at execution; _checkReplacement only requires the new answer inside the existing band, so old and new answers may differ by up to 16x. Because anyone may execute a Ready proposal, an attacker controls the exact moment of the repricing and can wrap it in one transaction: deposit at the stale NAV, executeProposal, redeem at the corrected NAV. The round-trip fee is 1%; the gain is the attacker's fraction of the revaluation.
Accepted design (1) covers feed lag, but here the lag is known 7 days in advance (the proposal is public), the correction size is known, and the trigger is in the attacker's hands, so it is riskless rather than speculative. Noted as a design observation: a mitigation that keeps the agreed design is operational (owner pauses deposits across the Ready window, or executes at the first Ready second), not a code change.
Genesis with 4 assets at $100.
Alice deposits 1000e18 A ($100k) day 1 and 1000e18 B ($100k) day 2: NAV $200k.
Owner proposes feed F' for A reading $300 (inside band [25,400]).
At Ready, Bob in one tx: deposit(B, 1000e18) = $100k (bucket limit max(300k/4,100k) = 100k) -> executeProposal(id) -> redeem(all).
Measured: Bob receives legs worth $165,279 at the new prices for $100,000 in; Alice bears the $65k.
Scratch test testFeedReplacementSandwichProfit on this tree.
Retirement execution drops NAV by the retired asset's value while its tokens stay redeemable; bundled deposit after execute extracts it (accepted design 4, numbers)src/BaskVault.sol:413
At retirement the asset counts 0 in NAV but its managed balance is still paid out pro rata by redeem. Anyone can execute the Ready retirement and in the same transaction deposit at the depressed NAV and redeem, taking a fraction of the retired tokens from existing holders. Accepted design (4); recorded so the operator knows the magnitude: with the retired asset at one third of the book the attacker clears ~32% on the maximum bucket deposit after fees.
The exposure only exists when the retired token is still transferable (retired for feed reasons rather than because it is frozen).
Genesis with 4 assets at $100.
Alice deposits $100k each into A, B, C on three days (NAV $300k).
Owner closes A and proposes retirement.
At Ready, Bob in one tx: executeProposal(retire A) (NAV -> $200k) -> deposit(B, 1000e18 = $100k; bucket limit max(300k/4,100k) = 100k) -> redeem(all).
Measured: Bob's legs are worth $132,223 for $100,000 in.
Scratch test testRetireSandwichProfit on this tree.
Deposit bucket is never released by redeem, so a deposit-and-redeem round trip blocks all other deposits for a day at ~1% costsrc/BaskVault.sol:555
The global bucket only grows (by deposit value) and decays linearly over 24h; redeem does not reduce it. An attacker can deposit the whole allowance max(NAV2/4, 100k) and immediately redeem, leaving the bucket full while NAV returns to its prior level. Every other deposit then reverts BucketExceeded until decay frees room (1/24 per hour), and the attacker can top up hourly.
Cost is the 0.5% deposit fee plus 0.5% redeem fee on the bucket amount: at NAV $100k the measured cost is $749 per day to deny all deposits; at NAV $1M it is ~$2,500 per day (1% of NAV/4). Spec defines the bucket as add-only, so this is a griefing property of the agreed mechanism rather than a coding error; reported for the operator's awareness.
Genesis with 4 assets at $100.
Alice deposits 1000e18 A ($100k).
Next day Bob deposits 1000e18 B ($100k, exactly the limit) and redeems all his shares in the same block. bucket() == 100_000e18 while NAV is back to ~$100k.
Alice's deposit(C, 1e18, ...) reverts BucketExceeded.
Bob's measured cost: $749.37 of his $100k.
Scratch test testBucketGriefingCost on this tree.
Surplus token balance from corporate actions (splits, in-kind dividends, returned seizures) is stranded forever because managed never increases outside depositsrc/BaskVault.sol:628
Redeem pays min(managed, available) * net / supply and NAV values only managed. Any balance the vault receives without a deposit is invisible: it is never distributed and never priced. For Stock Tokens this is not exotic: a 2:1 split that credits holders with additional tokens halves the feed answer, so NAV halves while the extra tokens sit unreachable; a token dividend paid in kind is likewise lost.
The spec says tokens sent directly are ignored, so this is a design consequence, but it means a realistic corporate action permanently destroys half the holders' value in that position, with no owner lever to recover it (no sweep or re-sync exists by design).
Alice deposits 1000e18 A at $100.
Issuer performs a 2:1 split: vault balance becomes 2000e18, feed set to $50.
Alice redeems all her shares.
Measured: vault still holds 1005e18 A with managed 5e18 (fee dust); 1000e18 A (worth $50k) is permanently unreachable by any function.
Scratch test testSplitSurplusStranded on this tree.
Permissionless recognizeLoss on a transient freeze zeroes managed; when the balance returns the whole position is strandedsrc/BaskVault.sol:681
flagDeficit/recognizeLoss treat a readable zero balance as a real loss after 7 days, and anyone can call them. A compliance hold or issuer freeze that makes balanceOf(vault) read 0 for a week (while the tokens are later restored) lets any third party write managed to 0. Deposits then price the asset at nothing, and when the hold lifts the restored tokens are outside managed, so redeem pays 0 for that asset forever (see also the surplus finding).
Nothing distinguishes a temporary hold from a seizure, and the owner cannot block the recognition (pausing deposits does not gate recognizeLoss). Spec-faithful; flagged because the irreversible write-down is triggered by an unprivileged party on an event the owner may know to be temporary.
Alice deposits 1000e18 A.
Issuer sets the vault's balance to 0 (freeze).
Anyone calls flagDeficit(A); 7 days later anyone calls recognizeLoss(A): managed[A] == 0.
Issuer restores the 1000e18 balance.
Alice redeems all: leg for A == 0, vault still holds 1000e18 A that no function can ever pay out.
Scratch test testTransientFreezeThenRecognizeLossStrands on this tree.
- reviewed
#1572Audit judgeClaude7 findings · 1 mediumrevised
afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flowThe review is complete.
.imd-findings.jsonholds 7 merged findings and a 30-row coverage record (all 27 entry points answered, plus three invariants). No tracked file was changed; only the findings file andtest/scratch/were written, both git-ignored.What I kept, after reproducing each against the code
- Medium (with proof):
claim()and thepayLegframe still readbalanceOfunder the 50,000-gas cap, so an owed leg of a Stock Token whosebalanceOflater needs more gas can never be claimed, however much gas the caller supplies. The brief says claim pays "with no gas limit". I ran the specialist's proof and it failed as stated, but it also asserted the leg is deferred before claiming, which the simplest fix would break. I rewrote it to assert only the end state, confirmed it fails now and passes on a patched copy. - Low, merged from four specialists: the ZeroNAV guard plus the unburnable 0xdEaD shares make "every unretired asset at managed 0" a terminal, redeem-only state. Spec-faithful, so it needs a scope decision or an operator rule.
- Low: with feeRecipient unset the real round-trip cost is about 0.5%, not the 1% the accepted-design note states. Measured 54 bps unset vs 99 bps set.
- Low: permissionless
executeProposallets anyone bundle deposit, feed replacement and redeem atomically. Measured $165,279 out for $100,000 in. - Low: redeem never releases the deposit bucket, so a 1% round trip blocks all deposits for a day.
- Low, merged from three reports:
managednever increases outside deposit, so splits, in-kind dividends, or a seizure reversed afterrecognizeLossare stranded. - Info, merged from three reports: accepted design 4 quantified, a retired asset is paid pro rata by redeem but counts 0 in NAV.
What I dropped or recalibrated: the economics specialist's medium on ZeroNAV is downgraded to low since no funds are lost and the brief mandates the rule. All static-analysis leads are false positives (calendar arithmetic, intended XOR in mulDiv, intended strict equality).
My own pass found nothing new. Every role lever (pause, close, retire, cap, fee recipient, guardian or owner change) was traced and none touches redeem's or claim's path. The 64-asset redeem suite stays under 28M gas, runtime is 22,203 bytes, and constructor literals match the brief.
ran onclaude · claude-fable-5-1 · 20 turns · 13m 59s · 290 in · 46.4K out · 1.1M cachedsubmissionb476ef0b908a00bb64a34439630127aa0cb0855dd5cfe1eb1a94d8af6883d668device6b0a28df3d585600055efbf5af60f9a74c60e4b0c831789748389b5ca63b0ce9started from2a8374c161bce5cd161b1e1a52babca831d8bd25bundlenoneapplied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91a, 9cfc5d0bce72368c945488a19181d6f81e3788022105fd68d985e3cda07f5949, dd5fc922a83a27f14da93dc2a004554c2d587dfd2befa1fb9acc0f6f65c72466changed · 0 filesnothingmediumclaim() and payLeg still read balanceOf with the 50,000-gas cap, so an owed leg of a token whose balanceOf needs more gas can never be claimedsrc/BaskVault.sol:654
proof · a Foundry test the fix has to passNAV == 0 with nonzero supply is terminal: once every unretired asset has managed == 0, deposits can never resume, even into newly listed assetssrc/BaskVault.sol:794
While feeRecipient is unset the deposit fee is not minted, so the depositor recovers part of it and the real round trip is about 0.5%, not the 1% stated for accepted design (1)src/BaskVault.sol:566
At deployment feeRecipient is unset and setFeeRecipient is optional. The deposit fee is deducted from the depositor's shares (lines 575-577) but minted to nobody, while NAV rises by the full deposit value, so the share price rises for all holders including the depositor. A depositor who ends up owning fraction p of supply immediately recovers about p * 0.5% of the deposit.
The redeem fee is burned (line 603) and lost, so the round-trip cost floor is 0.5%, not the 1% the README and accepted design (1) state as the threshold at which a lagging feed becomes exploitable. For a depositor that is large relative to the pool the exploitable lag window is roughly twice as wide as documented.
Consequence of the brief's 'not minted while unset' rule: a documentation/operations item (set feeRecipient at launch, or state the 0.5% floor), not a code change request. Reproduced from audit_math.
Permissionless executeProposal lets anyone bundle deposit, feed replacement and redeem in one transaction to capture the repricing risk-freesrc/BaskVault.sol:397
Deposit bucket is never released by redeem, so a deposit-and-redeem round trip blocks all other deposits for a day at about 1% costsrc/BaskVault.sol:555
The global bucket only grows by deposit value and decays linearly over 24h; redeem does not reduce it. Anyone can deposit the whole allowance max(NAV2/4, $100,000) and redeem at once, leaving the bucket full while NAV returns to its prior level. Every other deposit then reverts BucketExceeded until decay frees room (1/24 per hour), and the attacker can top up hourly.
Cost: 0.5% deposit fee plus 0.5% redeem fee on the bucket amount, $749 per day at NAV $100k, about $2,500 per day at NAV $1M. The brief defines the bucket as add-only, so this is a griefing property of the agreed mechanism, reported for operator awareness. Reproduced from audit_economics.
Genesis with 3 assets at $100.
ALICE deposits 1000e18 of A0 ($100k).
Next day BOB deposits 1000e18 of A1 ($100k, exactly the limit) and redeems all his shares in the same block. bucket() == 100_000e18 while NAV is back to about $100k.
ALICE's deposit(A2, 1e18, ALICE, 0, now) reverts BucketExceeded. test/scratch/Review.t.sol::testBucketGrief asserts bucket() == 100_000e18 and the BucketExceeded revert.
managed never increases outside deposit, so balance that arrives otherwise (stock splits, in-kind dividends, a seizure reversed after recognizeLoss) is stranded foreversrc/BaskVault.sol:627
Accepted design (4) quantified: a retired asset counts 0 in NAV but is still paid pro rata by redeem, so the next depositor extracts the retired position from existing holderssrc/BaskVault.sol:779
Note only, per the brief. Deposit prices shares against NAV that excludes the retired asset, but redeem (lines 609-611) iterates every asset in the permanent list including retired ones. The difference is a transfer from existing holders to whoever deposits after retirement, bounded per day only by the bucket and the 1% round-trip fee, and repeatable daily until the retired position is drained.
With the retired asset at 50% of pre-retirement NAV a $10,000 deposit leaves with about $14,887 of tokens; with the retired asset dominant, the next depositor takes about 99.4% of it. Anyone can also execute the Ready retirement and bundle deposit+redeem in the same transaction. The exposure exists only while the retired token is still transferable.
Merged from audit_permissions, audit_math and audit_economics.
Operational: pause deposits before retiring any asset with a material managed balance.
Genesis with 4 assets at $100.
ALICE deposits 100e18 of A0 and 100e18 of A1 (NAV $20,000).
Owner closes and retires A0 (7-day proposal): NAV $10,000 while the vault holds 100e18 A0.
BOB deposits 100e18 of A1 ($10,000) and redeems all his shares: legs 49.625e18 A0 + 99.25e18 A1 = $14,887 for $10,000 in. test/scratch/Review.t.sol::testRetireExtraction logs leg0 49625313283208020050, leg1 99250626566416040100.
- Medium (with proof):
- updated
#1512Build contract projectCodex4 files changed
Fixed uncapped balance reads for claims and payouts, with three regression tests. Documented advisory risks while preserving the specified economics. Recorded all seven findings in
.imd-responses.json.Validation passed:
forge build,forge test(69 tests), andforge fmt --check. Maximum measured 64-asset redemption: 22.80M gas, below 28M.ran oncodex · gpt-6-astra · 5 turns · 5m 8s · 95.4K in · 10.9K out · 938.8K cachedsubmission675a8fde125ed64e7ab8b94ea6dce55eec9e8cb7e6e9ef3616c7e1e39b860d59device41ff1f83c6e68e787d9aff4d245c23f5ea0b2b11ea9ffe3d1a1bb9f5a02edcdbstarted from5b1785afc5190c01115ef4e6cd8bc8f8a27b6c35bundle1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91a · 108 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 4 filesREADME.mdsrc/BaskVault.soltest/BaskAdversarial.t.soltest/mocks/StockMocks.sol - updated
#1116ManifestCodexruntime erroron the agent's machine: the task produced no changes; the agent's last message was: `launch.json` already matches the accepted …retried on #309 (Codex)
the task produced no changes; the agent's last message was:
launch.jsonalready matches the accepted implementation and required literal addresses, so no revision was needed.- Schema and constructor ABI checks passed.
forge buildsucceeded; all 60 tests passed.- Runtime: 22,258 bytes.
- Tested 64-asset redemptions stayed below 28 million gas.
No tracked files changed.
ran oncodex · gpt-6-astra · 3 turns · 2m 1s · 44.3K in · 3.6K out · 337K cachedsubmission3ff9539a723222c7da93cf64b2cf79736e47b610e2ce0196fb7e0deeae05f116devicea9158e223bd79f257c029b3ee1b31db6956106bb2f7bdec6d7fd4c15c6a577adstarted from7bba2f0c753947ba0a99474c97699b69c6a36618bundlenoneapplied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 0 filesnothing#309Codex1 file changed
afterBuild contract projectwrites tolaunch.jsonUpdated launch.json to clarify redemption gas limits and deferred claims.
Schema and constructor checks passed.
forge buildsucceeded; all 60 tests passed, including hostile-token redemptions below 28 million gas.Only
launch.jsonchanged.ran oncodex · gpt-6-astra · 4 turns · 2m 6s · 38.4K in · 4.3K out · 323.2K cachedsubmissionc000c4a4cae6aa3eae572a9c17e26903c9a0528f7d11b27a3546352433cd40d5deviceeaa98ad0794d4f7600fe2da071ebf65e46f4c0f989bd53e39cff0d2158507704started fromc3d38d50bf6b7254215e61af6480f447034fd1d2bundledd5fc922a83a27f14da93dc2a004554c2d587dfd2befa1fb9acc0f6f65c72466 · 110 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 1 filelaunch.json - updated
#1383Write foundry testsCodex3 files changed
afterBuild contract projectwrites totesttest/**Added regression tests for redemption rollback across paid and deferred legs, and failed listing execution preserving its proposal and daily allowance.
forge buildsucceeded.forge test: 70 passed, 0 failed, including 1,000 new fuzz cases and 24,576 invariant calls.Changes are limited to
test/. No new contract defects were reproduced.ran oncodex · gpt-6-astra · 6 turns · 7m 56s · 131.4K in · 7.2K out · 1.9M cachedsubmission31115b1cf5176de25131e376ba645691578a862a302a9d44ce926c13181c8812devicec4f9adf8d634c7961729c3832d70a12544e3895648e7cf2721a6093f5312fef1started from2d8cadcf0c361ffb0aaf6ad2c5fccca23f5f0e45bundle9cfc5d0bce72368c945488a19181d6f81e3788022105fd68d985e3cda07f5949 · 120 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91achanged · 3 filestest/BaskAdversarial.t.soltest/BaskGovernance.t.soltest/STATEFUL_TESTS.md - reviewed
#683Audit judgeCodexno findings
afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flowWrote
.imd-findings.jsonwith no unresolved findings and 27/27 entry points covered.Previous finding Resolution Claim balance-read gas cap Fixed; supplied proof and expensive-read regressions pass. Terminal ZeroNAV Reproduced; documented specified behavior. Unset fee recipient economics Reproduced 54 versus 99 bps; documentation corrected. Atomic feed replacement repricing Reproduced; operational guidance documented. Deposit bucket consumption Reproduced; documentation correctly explains decay. Unmanaged surplus/restored losses Both cases reproduced and documented. Retired-position extraction Reproduced; accepted-design note documented. All 70 retained tests and eight advisory reproductions passed. Maximum measured 64-asset redemption: 22,795,771 gas.
The older specialist proof fails only because it expects deferred payment where redemption now pays immediately. It does not reproduce stranded funds.
Scratch tests were removed; source and configuration remain unchanged.
ran oncodex · gpt-6-astra · 5 turns · 8m 32s · 124.5K in · 9.3K out · 1.1M cachedsubmission37e2565be549f97ead229e8ffe3b530fdbfd940b64feef13df53a0fbc67df6aedevice42dc54b22317eb8178422a6c839e11256b13030b4dfc702832024c1667ad3db1started from12c6bed38fa88040a33dde08cd6d0bb932583c81bundlenoneapplied on1442422b2d2b456668f4627945d657541c39ab5707b38146b511d6fbee51c91a, 9cfc5d0bce72368c945488a19181d6f81e3788022105fd68d985e3cda07f5949, dd5fc922a83a27f14da93dc2a004554c2d587dfd2befa1fb9acc0f6f65c72466changed · 0 filesnothing - publishedidentity-md-launches/launch-929-basketpull request
- deployed
1 contracton Robinhood Chain, 7 gates passedtransaction
- rebuilt
- BaskMath, BaskVault · verifier 0.1.0 · solc 0.8.26
- gates
- provenance
- findings
- independent review
- bytecode
- manifest
- protected invariants
- economics
- proof
commit, attestation, manifest, tree, per-contract hashes
- repository
- identity-md-launches/launch-929-basket
- commit
- b12f8ecdaac0acc13e47646441b4f312a2aab160
- attestation
- 9806e8957d7e7f39ff773d70858997de2fdb927230b92aef96ddcc2b723fc7a9
- manifest
- bc600bf29e9dc71c748980c15a1fe1e5311668b7a458efc846d78db614708565
- constructor
- BaskVault: 0x30B57ECf51D19ABcED7F6f70974e6fBb6f3b9Da3, 0x5ed39AF86f2C00ad99913B5d727bD68f2A904B68
- tree
- f5641f84dc238aa58744336c01965438ed89a1fc
- compiler
- solc 0.8.26, optimizer 200 runs, via-ir, reproducible
- contract
- BaskMath
src/BaskMath.sol · 44 bytes
creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
abi 5ff5499febb7d544e4e1909348158dd92a5c67d231bfbd8960fdd2f087d4fc45
metadata c9e2badd5348253ee6aa8761e79d871bdacc35d6ddec93b85985c300b1dc80f8 - contract
- BaskVault
src/BaskVault.sol · 22733 bytes
creation 91f8b8be1b7652a89e5dbbe75a6e0aa2fe22c48b63ba738359edc50908209829
abi 1e1c867f8cc1a41e02aa84676c0358fbc22001f609427068414dc24c40b57732
metadata 2ffa67fb52b279d780c6eaa3a86837a56ab9b26798e05eed2159962cdc5fc65d
onchain at 0xd77a…764c, block 82,708,976 · creation code matches
- onchain