Skip to content

Name ignoring donations as the vault strategy's first-depositor defense - #96

Merged
mikemaccana merged 6 commits into
mainfrom
claude/laughing-mayer-kkl3q2
Sep 22, 2026
Merged

mikemaccana merged 6 commits into
mainfrom
claude/laughing-mayer-kkl3q2

Conversation

@mikemaccana

@mikemaccana mikemaccana commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Merge order: this PR must merge after the companion PR quicknode/solana-program-examples#144. That PR makes the vault strategy record what it holds and price shares from those records, ignoring donations. Every identifier and number below describes what it ships, so merging this first would have the book describing a defense the companion had not shipped.

An earlier version of this PR described virtual shares. The companion switched to ignoring donations, which is simpler for this vault and keeps the one-to-one first deposit and every existing number, so this version replaces it.

What changed

  • Vault Strategy chapter
    • The account model names usdc_holdings and asset_holdings.
    • The deposit values the fund from those records rather than vault balances. It records what each swap moved, and refuses a swap that returns nothing (DepositTooSmall).
    • Withdrawal legs and the math section use the recorded holdings. The invariants gain one: prices and payouts come from the records, which never exceed the vault balances.
    • The failure mode "The first deposit has no dead-shares guard" becomes two bullets:
      • Donations are ignored, run against the Adversary chapter's numbers: the next depositor's 1,000 USDC still buys 1,000 shares, and the donation is never paid out. rebalance cannot sell or spend donated tokens (InsufficientHoldings).
      • A deposit too small to buy anything is refused. minimum_shares remains for a share price that moved between signing and landing. The pointer to the Adversary chapter's empty-pool section stays.
    • The testing section names the four new or extended tests, and the summary bullet says to value what the program recorded.
  • Adversary chapter, "The Empty Pool"
    • The walkthrough's bare pool is "priced from the token balance in its vault, as the book's vault strategy did before it chose a defense".
    • The minimum_shares paragraph says the vault strategy keeps the argument without relying on it.
    • The "Ignore donations" bullet names the vault strategy's recorded holdings next to the lending market's available_liquidity, and points at test_donation_does_not_inflate_share_price.
    • The closing line names the defense each of the book's programs chose.
  • Perpetual Futures chapter: the cross-reference that called the vault strategy's guard "missing" now says the Vault Strategy chapter ignores donations instead.
  • WRITING-PLAN.md, War Story Register: the Vault Strategy chapter teaches ignoring donations.
  • Figures: none changed. No vault figure draws a vault balance feeding the valuation, and the deposit figure's "900 USDC in, 900 shares out" still holds.

I grepped for one-to-one, dead-shares, virtual and missing guard. The remaining hits are the bare pool in the Adversary walkthrough, the vault's still-one-to-one first deposit, the AMM's and lending's own mechanisms, and the virtual-shares bullet describing that general defense. No line says the vault's first deposit is unguarded.

Relation to other PRs

Verification

  • npm run lint clean.
  • node scripts/check-figure-geometry.mjs clean.
  • npm run build-pdf wrote the book, 260 pages.
  • Every identifier the new prose names resolves in the companion PR: usdc_holdings, asset_holdings, DepositTooSmall, InsufficientHoldings, minimum_shares, and the four test names.

🤖 Generated with Claude Code

https://claude.ai/code/session_012jbHknFnC6NKkeWvgvziB1

The empty-pool section stated the mechanism in the abstract: a minuscule
first deposit, a donation, a division that floors to zero. Nothing in it
could be checked by a reader with a pencil. It now runs the attack against
a pool priced the way the vault strategy prices its shares: one minor unit
deposited through the handler, 1,000 USDC sent straight to the vault, and
Bob's 1,000 USDC deposit landing on the skewed rate to mint zero shares.
A figure draws the three states, with the vault balance as a bar and the
share supply stuck at one beneath it.

The same numbers then run through each defense, so the reader sees why a
locked minimum makes the donation cost a hundred times what it takes, why
virtual shares hand half the donation to nobody, and why a pool size kept
in program state leaves the donation outside the accounting. The vault's
own minimum_shares floor is named as the caller-side bound it is: it
protects the one transaction that sets it, not the pool.

"Going through deposit" is now "through the deposit handler", the book's
term for the function rather than its input.

Claude-Session: https://claude.ai/code/session_0154xpTf5MDvrp4m2qSVAtsn
Bob's step computed 1,000 × 1 ÷ 1,000.000001 with nothing telling the
reader where the three numbers came from. The formula now sits on its own
line before the steps, as the betting market's payout formula does, with
its terms named: deposit, shares outstanding, pool value. A sentence
before it says what the formula means (a deposit that doubles the pool's
value doubles the share supply), and Bob's step maps each of his numbers
onto a term before substituting. The 1,500 USDC variant and the figure's
annotation use the same form, and the opening paragraph's "supply" and
"value" now match the formula's names.

Claude-Session: https://claude.ai/code/session_0154xpTf5MDvrp4m2qSVAtsn
The figure's headline said Bob's deposit bought no shares, without saying
who did it or what it earned them. It now reads "An attacker makes Bob's
deposit buy no shares", and step 3 draws the payoff: a bracket across
both deposits marks the attacker's one share as redeeming for all
2,000.000001 USDC, and a bold line under it states the result, 1,000.000001
USDC in and 2,000.000001 out.

The prose had the same gap in the same place. Bob's step ended on "the
attacker holds the share" and left the profit to the paragraph after the
figure; the profit now closes Bob's step, so the reader has it before the
figure, and the following paragraph no longer repeats it.

Claude-Session: https://claude.ai/code/session_0154xpTf5MDvrp4m2qSVAtsn
Audited against the companion AI plugin's writing rules. The chapter leaned
on metaphor where a mechanism was available ("every mistake is a bounty",
"attacks are industrialized", "the boundary values are where the money
is", "fantasy prices", "renting one block"), closed paragraphs on quotable
restatements, used "X is not Y; it is Z" where nobody believed Y, and ran
lists together inside paragraphs. Each is replaced by the literal claim:
a flaw worth a cent per call is worth $10,000 to a script that calls it a
million times; the price-manipulation defenses are a list.

The empty-pool section loses its invented labels ("degenerate zone",
"named defense", "relies on circumstance") and its closing sentence about
the vault strategy. The locked-shares bullet now states the design its
numbers assume, locked shares counted in every divisor, instead of
attributing them to the AMM, whose deposit formula leaves the floor out.
The virtual-shares numbers are recomputed: Bob buys 1,999,999 shares
redeeming for 999.99975 USDC.

The Wormhole story now says what the deprecated function failed to check,
the Hundred and Sonne story is sentences instead of a semicolon list, and
two headings say what they mean: "Every Mistake Is Money Anyone Can Take"
and "A Rounding Error Repeated Enough Times Drains the Pool".

Claude-Session: https://claude.ai/code/session_0154xpTf5MDvrp4m2qSVAtsn
The Adversary chapter conflicted with four edits main made while this
branch was open. The branch's plain-language rewrite already covers most of
them; three are carried into it: the stored-address check and its
per-framework footnote replace the has_one pattern, the order book is
described as checking every vault against its market's recorded
addresses, and the Cashio footnote takes main's wording.

Main's new lint rules also caught two things on the branch: the
inflation-attack figure used curly-apostrophe entities where figure text
now takes straight ones, and two sentences in the Hundred and Sonne story
used "drained" without naming the attacker.

Claude-Session: https://claude.ai/code/session_0154xpTf5MDvrp4m2qSVAtsn
The companion repository's vault strategy now records what it holds,
usdc_holdings and asset_holdings, and prices shares and pays withdrawals
from those records rather than its vaults' token balances, so a donation
cannot move the share price. It also refuses a deposit whose swap returns
none of its asset (DepositTooSmall).

The Vault Strategy chapter describes the records in the account model, the
deposit, the withdrawal, the math and the invariants, replaces the "no
dead-shares guard" failure mode with the donation defense and the dust
deposit guard, and names the new tests. The Adversary chapter's empty-pool
section names the vault strategy alongside the lending market under
"Ignore donations" and says which defense each of the book's programs
chose; the Perpetual Futures cross-reference and the War Story Register
follow.

Claude-Session: https://claude.ai/code/session_012jbHknFnC6NKkeWvgvziB1
@mikemaccana
mikemaccana force-pushed the claude/laughing-mayer-kkl3q2 branch from 9f77941 to 145176e Compare September 22, 2026 23:45
@mikemaccana mikemaccana changed the title Name virtual shares as the vault strategy's first-depositor defense Name ignoring donations as the vault strategy's first-depositor defense Sep 22, 2026
@mikemaccana
mikemaccana merged commit bfe0c28 into main Sep 22, 2026
1 check passed
@mikemaccana
mikemaccana deleted the claude/laughing-mayer-kkl3q2 branch October 13, 2026 00:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant