diff --git a/WRITING-PLAN.md b/WRITING-PLAN.md
index 6646fec1..928ee09c 100644
--- a/WRITING-PLAN.md
+++ b/WRITING-PLAN.md
@@ -6,13 +6,13 @@ How the manuscript gets produced, verified, reviewed, and kept alive. The table
- The book prints the durable layer. Every code claim is verified against the companion repository before it lands: the skill's "grep before naming" rule, applied to a manuscript.
- Diagrams are the longest-lived medium. Every chapter budget includes them: a state machine, and a figure for every walkthrough step. The step figures share one column layout and accumulate into the program's full account map, so no map is drawn up front.
-- Style is inherited wholesale from the [companion AI plugin](https://github.com/quicknode/solana-finance-claude-plugin): onchain and offchain unhyphenated, "instruction handlers" for the functions and "instructions" for their input, "minor unit" not "base unit", "network" for Solana and "program" for onchain code with "protocol" never used for either, one name per role enforced within each chapter, concrete two-asset examples, real handler names in lifecycle prose, persona-driven walkthroughs. Also from the skill, and deliberately not repeated below: handler naming (`initialize_x()` for setup, the domain's verb otherwise), PDA versus account, mint versus token, example assets, plain language and concrete definitions, emphasis, stating the mechanism rather than gesturing at it, and talking to readers rather than about the document. **A rule that would apply to any Solana finance writing belongs in the skill; this file adds only what is specific to the book.** `scripts/lint-manuscript.mjs` still enforces several of the skill's rules over the manuscript, so a rule can live there and be guarded here.
+- Style is inherited wholesale from the [companion AI plugin](https://github.com/quicknode/solana-finance-claude-plugin): onchain and offchain unhyphenated, "instruction handlers" for the functions and "instructions" for their input, "minor unit" not "base unit", "network" for Solana and "program" for onchain code with "protocol" never used for either, one name per role enforced within each chapter, concrete two-asset examples, real handler names in lifecycle prose, persona-driven walkthroughs. Also from the skill, and deliberately not repeated below: handler naming (`initialize_x()` for setup, the domain's verb otherwise), PDA versus account, mint versus token, example assets, plain language and concrete definitions, emphasis, stating the mechanism rather than gesturing at it, and talking to readers rather than about the document. **A rule that would apply to any Solana finance writing belongs in the skill; this file adds only what is specific to the book.** `scripts/lint-manuscript.mjs` still enforces several of the skill's rules over the manuscript, so a rule can live there and be checked here.
- Banned phrases, enforced by `scripts/lint-manuscript.mjs` (`npm run lint`) over the manuscript files: "the oracle problem", "instrument" (say "financial software" or "program"), the hyphenated chain words, and em-dashes. The lint also bans a group of phrases that read as machine-written, on the same grounds as the em-dash: a reader clocks them before finishing the sentence, and every one of them stands in for a mechanism rather than stating it. "Load-bearing" is the type specimen; say what depends on the thing and what breaks without it. The rest are "delve", "tapestry", "intricate", "multifaceted", "holistic", "myriad", "seamless", "robust", "pivotal", "crucial", "elevate", "testament to", "deep dive", "paradigm shift", "game-changer", "unlock the power", "realm of", "landscape of", "in today's", "it is worth noting", and "underscores the". A second group is banned for announcing that a point exists instead of making it, each one written in this manuscript and each replaced by the content it stood in front of: "what matters", "the point is", "that is the whole reason", and "does the work of". "What matters here is the unit" became "the index advances by slots, not seconds"; delete the announcement and keep what followed it. The same announcement came back wearing a pronoun once the plain forms were banned, so "that is the point", "which is exactly the point", "is the one that matters", "matters more than it looks" and "it is worth being" are banned too. A related group points the reader at the page instead of telling them what is on it: "Note what is pledged", "Notice who did nothing", "Watch K in the story below". Each is one word longer than the sentence that says the thing, and the fix is to say it. Watching a real market or looking away from the chain is the reader doing something in the world, and stays. "Treats" meaning "discusses" ("the *Lending and Borrowing* chapter treats it fully") is banned; say "explains" or "covers". "Treats X as Y" is a different verb and stays. "Pin" is banned in both of its uses here: a test enforces, checks, or asserts, and a value is fixed or recorded at creation. "Capstone" is banned for rating a chapter's place instead of saying what it does; say "this chapter" or name it. "Walks" with a chapter, book, or suite as its subject is banned as tour-guide framing: a chapter explains or builds, and a suite follows or runs a path; a loop that walks a tree or the states of a machine is mechanism and stays. "Works" meaning succeeds or functions stays ("the deadline arithmetic flips which handler works"); what is banned is working an example ("the chapter works an example"), where the chapter shows or builds it instead. A third group rates the writing instead of doing it, and each was found many times over in one audit: "honest" and "honestly" (twenty uses, none about honesty), "quietly" as an intensifier, "the discipline" and "hygiene" as names for a rule, "by now you", and "worth stealing", "worth copying", "worth noticing", "worth printing", "worth studying". The paragraph-closing maxim ("A wall that has never been pushed on is a hypothesis") is the same tell with no regex to catch it, so the companion AI plugin describes it and the lint does not. A fourth group is mannered prose: an idiom, a slang word, or a metaphor standing where a literal phrase of the same length was available, and each was found in one audit: "a single whale" (a single large backer), "config foot-guns" (dangerous configurations), "zombie positions", "cannot be rugged", "by fat-finger", "after the whistle", "mid-flight", "the hot path", "hand-rolled", "comes off the top", "pays for itself", "comes home", "the ladder has three rungs". A metaphor drags in connotations the writer did not choose, so the fix is to say what you mean. The teaching analogy that carries a mechanism stays (a mint is a factory, Solana is one giant computer), and so does the finance term the field uses (a deep pool, seeding a pool, the rails); the lint catches the words above, and the companion AI plugin describes the rule in general. "Fail open" and "fail closed" are banned for a different reason: an open circuit stops and an open door lets everything through, so readers split on which way each one points. Say what the program does instead: it refuses the transaction, or it lets every request through. The lint also rejects negative framing of offchain truth (any "cannot verify the price" phrasing): the manuscript teaches the positive mechanism, checking a fact's provenance, freshness, and confidence against a source the program trusts. "Property test" is banned in favor of "invariant test", the companion AI plugin's rule, because "property" also names an account's fields; the one exception is the testing chapter's single pointer to the libraries' own term, "property-based testing". "Smart contract" is banned in favour of "program", with exactly two exceptions, both contrastive: the Introduction's line telling a reader arriving from another chain what a Solana program is, and the glossary's definition.
- A key pair is two keys, and only its public key is an address: prose never calls a key pair an address or an account, never puts one on or off the curve, and never says a PDA has one. In the figures, an address made from a key pair is an unlabeled `r="6"` dot on the curve with its account's box beside it; it is never an offcurve dot with a caption, and no caption names a key pair. `scripts/lint-manuscript.mjs` fails the prose and the captions, and `scripts/check-figure-geometry.mjs` fails a public-key dot off the curve, a derived-address dot on it, and any dot more than 20 units from an account box. Seven order-book figures once drew the order book and its vaults as offcurve dots captioned `A CLIENT KEYPAIR`. The vaults are now PDAs of the market; the book, which the client must create, has its box above the curve at the top right, beside its dot.
- No sentence runs past forty-five words, enforced by `scripts/lint-manuscript.mjs` over prose, list items, and boxouts. The ceiling is deliberately loose, because it is catching a specific failure rather than policing rhythm: every sentence it first caught was a list that had been glued into a paragraph, or two separate claims joined by a colon, and the fix each time was to split at the punctuation already sitting there. The same pass bans a short list of words that have a plain equivalent and carry no meaning of their own here: "utilize", "commence", "prior to", "subsequent to", "in order to", "in the event that", "due to the fact that", "whilst", "amongst", "thus", "hence", "therefore", "approximately", "numerous", and "sufficient". Words that only *look* bureaucratic stay, because they are exact in this domain: a transaction **executes**, a supplier earns **yield**, a maker **provides** liquidity, a program signs by **presenting** seeds, and a firm **operates** a venue. "Confirm" is banned as a loose synonym for "check", because `confirmed` is one of Solana's three commitment levels and the book teaches them. This is the one rule taken from ASD-STE100 Simplified Technical English; the rest of that standard is written for aircraft maintenance procedures, and its bans on direct address, rhetorical questions, and metaphor would flatten a book that teaches with a story.
- An address is written as a markdown link, `[host.tld](https://host.tld)`, never as loose prose, because the PDF build only reaches links. It prints in italic, which is what technical publishing does with an address (O'Reilly's house style and No Starch's) and needs no machine face to tell a reader it is copied exactly; the monospace face stays with `program_id`, where it separates code from the prose around it. Never underlined: underlining is a screen convention and collides with descenders in a serif face. The scheme prints, because it is part of the address and because technical publishing prints one: O'Reilly sets a link as "words (https://host/path)". A leading `www.` does not, being a subdomain nobody types and every browser supplies. The link keeps whatever it was written with, and the link is what a click follows, so write the link text as the host and path alone and let the markdown target carry the full address. A link whose text is words rather than an address gets that address printed after it in parentheses, since nobody can click a printed page. Both are done by `scripts/build-pdf.mjs`; the lint catches an address written as prose. A long address may break across lines, at a slash; what may not break is where it points, so `npm run build-pdf` reads the link annotations back out of the finished PDF and fails unless every line of a broken address carries the whole destination. But an address does not belong in running prose at all: it is a string of punctuation inside a justified measure, and a reader holding the book cannot act on it while reading the sentence anyway. It goes in a footnote, which names the page it points at, so the sentence keeps its words and the address waits at the foot of the page for a reader who wants it. Two things stay inline. `solanabook.org` is short and is the thing its sentence is naming, and the Glossary is reference matter already, whose entries would read oddly with notes hanging off them.
-- American spelling throughout, which the lint guards in two batches: the "-our" and "-ise" endings, and the doubled-l forms and noun/verb splits British English keeps ("canceling", "modeling", "cancelable", "center", "license"). Identifiers keep whatever the companion repository shipped, so the `Cancelled` enum variant is written capitalized and left alone. Typography in the source is one apostrophe and one set of quotes, both straight, and one italic marker, the asterisk. The figures are typed the same way. The build sets the quotes the way a typeset book carries them, ’ and “ ” in prose, headings, and figure labels, and straight inside code, which is O'Reilly's convention and every other technical publisher's; the lint rejects a curly quote, as a character or an entity, in a chapter or a figure, so that grep and the build only ever meet one form. A single quote is always an apostrophe, never a quotation inside a quotation, and the lint holds the manuscript to that too. Underscore italics are not a second house style: an underscore is a word character, so `\b` never fires against the start of `_Arithmetic of Money_`, and a chapter reference written that way reads clean to the cross-reference check. The lint bans them for that reason.
-- The publisher is **Quicknode**, one word, that capitalization. The legal entity is **Quiknode Inc.**, and it appears only where the entity is what is meant: the copyright holder, the liability disclaimer, the trademark owner, and the exclusive right to sell editions. There is no "Quicknode Press" imprint. `scripts/lint-manuscript.mjs` guards the spellings across the manuscript; `LICENSE.md` and `README.md` sit outside its globs and are checked by eye.
+- American spelling throughout, which the lint checks in two batches: the "-our" and "-ise" endings, and the doubled-l forms and noun/verb splits British English keeps ("canceling", "modeling", "cancelable", "center", "license"). Identifiers keep whatever the companion repository shipped, so the `Cancelled` enum variant is written capitalized and left alone. Typography in the source is one apostrophe and one set of quotes, both straight, and one italic marker, the asterisk. The figures are typed the same way. The build sets the quotes the way a typeset book carries them, ’ and “ ” in prose, headings, and figure labels, and straight inside code, which is O'Reilly's convention and every other technical publisher's; the lint rejects a curly quote, as a character or an entity, in a chapter or a figure, so that grep and the build only ever meet one form. A single quote is always an apostrophe, never a quotation inside a quotation, and the lint holds the manuscript to that too. Underscore italics are not a second house style: an underscore is a word character, so `\b` never fires against the start of `_Arithmetic of Money_`, and a chapter reference written that way reads clean to the cross-reference check. The lint bans them for that reason.
+- The publisher is **Quicknode**, one word, that capitalization. The legal entity is **Quiknode Inc.**, and it appears only where the entity is what is meant: the copyright holder, the liability disclaimer, the trademark owner, and the exclusive right to sell editions. There is no "Quicknode Press" imprint. `scripts/lint-manuscript.mjs` checks the spellings across the manuscript; `LICENSE.md` and `README.md` sit outside its globs and are checked by eye.
- Numbers from outside the companion repository (fees, incident losses, market sizes) name their source in the text, link the primary publisher, and say where and when they apply. A figure that contradicts a reader's own experience is the normal case rather than the exception, because a cost that is typical in one country is unheard of in another: state the scope, so a reader whose transfers have all been domestic can check the claim in one click instead of concluding the book made it up.
- Nothing in the book disparages other Solana resources or projects. Where other content is referenced, it is as a neutral pointer, and preferably to a primary source (a project's own docs or repo) rather than to an aggregator or list.
- Chapter titles are numbered traditional short names: "Chapter 3: Tokens", "Chapter 11: Escrow", "Chapter 9: The Adversary". Headings *inside* a chapter carry the content, so a reader skimming only the headings learns the chapter's actual claims: "A Mint Is a Factory, and the Mint Authority Is Its Boss", not "Mint Authority". A heading is close to a summary of its section, and has to work read alone, because the printed table of contents lists every one of them out of context. It states what the reader gets, not the mechanism that delivers it: "Seeds Make Data Findable", not "Seeds Make Addresses Derivable", because derivability is how it works and findability is what it buys. Title Case throughout. No numbered headings.
diff --git a/appendices/glossary.md b/appendices/glossary.md
index 2c169177..d689ae04 100644
--- a/appendices/glossary.md
+++ b/appendices/glossary.md
@@ -10,7 +10,11 @@ See [borrow accumulation factor](#borrow-accumulation-factor).
### address
-The 32-byte identifier of an account, usually shown base58-encoded. An address is one of three kinds: a public key, which has a matching private key; a [PDA](#pda-program-derived-address), derived from seeds, which has no private key; or a [built-in address](#built-in-address), chosen rather than computed, which has no private key either.
+The 32-byte identifier of an account, usually shown base58-encoded. An address is one of three kinds:
+
+- a public key, which has a matching private key
+- a [PDA](#pda-program-derived-address), derived from seeds, which has no private key
+- a [built-in address](#built-in-address), chosen rather than computed, which has no private key either
### adverse selection
@@ -32,6 +36,10 @@ Buying where an asset is cheap and selling where it is dear until the two prices
An order to sell at or above a stated price. The seller's side of an [order book](#order-book).
+### asset
+
+Something of value a participant owns and a program can hold for them, such as USDC or a stock like NVDAx or TSLAx. Assets on Solana are represented as [tokens](#token). The book says "assets" where no single token is meant, as in "every participant must be able to get their assets out of every state". See [RWA](#rwa-real-world-asset).
+
### associated token account (ATA)
The canonical token account for a given wallet and mint, at an address anyone can derive from three seeds: the owner's address, the token program that manages the mint, and the mint. ATAs are why a program can know where Alice's USDC lives without asking her.
@@ -98,7 +106,7 @@ Rounding up to the next whole [minor unit](#minor-unit). The direction used for
### checked arithmetic
-Arithmetic that reports failure instead of producing a wrong answer: Rust's `checked_add`, `checked_sub`, `checked_mul` return nothing when the result will not fit, so the handler can fail with a named error. The alternative is [wrapping](#wrapping). Every operation on money in this book is checked.
+Arithmetic that reports failure instead of producing a wrong answer: Rust's `checked_add`, `checked_sub`, `checked_mul` return nothing when the result will not fit, so the handler can fail with a named error. The alternative is [wrapping](#wrapping). Every operation on an amount in this book is checked.
### checks-effects-interactions
@@ -110,7 +118,14 @@ In a lending market, the fraction of a debt one liquidation may repay (typically
### cluster
-One running instance of Solana: the same software, a separate world. Mainnet holds real value; devnet is identical machinery with free, valueless tokens for building and testing; localnet runs on your own machine; testnet is where the Solana software itself is tested. An account exists on one cluster, not all of them. Each is reached at an [RPC endpoint](#rpc-endpoint).
+One running instance of Solana: the same software, a separate world.
+
+- **Mainnet** holds real value.
+- **Devnet** runs the same software as mainnet, with free, valueless tokens, for building and testing.
+- **Localnet** runs on your own machine.
+- **Testnet** is where the Solana software itself is tested.
+
+An account exists on one cluster, not all of them. Each is reached at an [RPC endpoint](#rpc-endpoint).
### collateral
@@ -155,7 +170,11 @@ A permissionless instruction handler that performs maintenance work nobody owns
### critbit tree
-A binary tree keyed on the first bit where entries differ. Used for order books because insertion never shifts other entries and walking it yields price-time priority for free. See the *Order Book Exchange* chapter.
+A binary tree keyed on the first bit where entries differ. Used for order books because insertion never shifts other entries and walking it visits orders in price-time priority. See the *Order Book Exchange* chapter.
+
+### crypto
+
+In the context of finance, the industry built on blockchains: distributed ledgers such as Solana. Crypto is not an asset class in itself, although the term is frequently misused that way. Crypto is a wrapper for trading a wide variety of asset classes, including equities such as NVDAx, commodities such as gold, derivatives such as the perpetual futures in this book, and native crypto assets such as SOL. Each of these is held onchain as a [token](#token).
### curator
@@ -163,7 +182,7 @@ In a fund, the authority that decides which assets may be held at all and which
### custody
-Holding assets that belong to someone else. A program takes custody when tokens sit in a [vault](#vault) whose authority is its PDA. Balances are custody and state is truth: what the program owes is what its own accounts record, never what happens to be in the vault.
+Holding assets that belong to someone else. A program takes custody when tokens sit in a [vault](#vault) whose authority is its PDA. What the program owes is what its own accounts record, never what happens to be in the vault.
### decimals
@@ -171,11 +190,11 @@ The number of minor units in one major unit of a token, declared on the mint. US
### degenerate value
-A value at which a formula or a check stops meaning what it usually means, almost always zero or empty. An empty reserve makes K zero, so `new_k >= old_k` accepts every outcome; a price of zero or a negative [mantissa](#mantissa) means a broken feed; an offer with nothing in it is structurally valid and worthless. Guard each one explicitly rather than trusting the general check to catch it. See [invariant](#invariant).
+A value at which a formula or a check stops meaning what it usually means, almost always zero or empty. A constant-product pool multiplies its two reserves to get [K](#k) and refuses any trade that makes K fall. If either reserve is empty, K is zero and that check passes whatever the trade pays out. A price of zero or a negative [mantissa](#mantissa) means a broken feed. An offer with nothing in it passes the usual checks but is worthless. Check for each one explicitly rather than relying on the general checks to catch it. See [invariant](#invariant).
### discretization
-Representing a continuous quantity as discrete integer steps. Onchain money is discretized by construction: amounts are integers in [minor units](#minor-unit), so there is no such thing as half a lamport, and every division must decide what happens to the fraction the result cannot represent. See [dust](#dust) and the *Arithmetic of Money* chapter.
+Representing a continuous quantity as discrete integer steps. Onchain amounts are integers in [minor units](#minor-unit), so there is no such thing as half a lamport, and every division must decide what happens to the fraction the result cannot represent. See [dust](#dust) and the *Arithmetic of Money* chapter.
### discriminator
@@ -219,7 +238,7 @@ Computing (1 + r)ⁿ for large n in log₂(n) multiplications instead of n of th
### faucet
-A service that gives away free test tokens on devnet: SOL from the `solana airdrop` command or [faucet.quicknode.com/solana](https://faucet.quicknode.com/solana), USDC from Circle's faucet at [faucet.circle.com](https://faucet.circle.com). Faucets are why nothing in this book needs real money.
+A service that gives away free test tokens on devnet: SOL from the `solana airdrop` command or [faucet.quicknode.com/solana](https://faucet.quicknode.com/solana), USDC from Circle's faucet at [faucet.circle.com](https://faucet.circle.com). Faucets are why nothing in this book needs mainnet tokens.
### feature
@@ -239,7 +258,7 @@ A loan borrowed and repaid inside a single transaction, possible only because tr
### floor
-Rounding down to the whole [minor unit](#minor-unit) below. The direction used for anything the program pays out, so withdrawals, winnings, and swap outputs never promise a fraction the program does not hold. The opposite of [ceil](#ceil-ceiling). See [rounding](#rounding) and [dust](#dust).
+Rounding down to the whole [minor unit](#minor-unit) below. The direction used for anything the program pays out, so withdrawals, winnings, and swap outputs never pay out a fraction the program does not hold. The opposite of [ceil](#ceil-ceiling). See [rounding](#rounding) and [dust](#dust).
### fork
@@ -251,7 +270,7 @@ The periodic payment between longs and shorts on a perpetual futures market that
### half-up
-Rounding to the nearest whole unit with exact halves going up, which is what Rust's `round` does. Never used on money here: it rounds in the user's favor half the time, and a caller can size a deposit and withdrawal pair to collect that half repeatedly. The 2021 SPL token-lending bug was this. See [rounding](#rounding).
+Rounding to the nearest whole unit with exact halves going up, which is what Rust's `round` does. Never used on amounts here: it rounds in the user's favor half the time, and a caller can size a deposit and withdrawal pair to collect that half repeatedly. The 2021 SPL token-lending bug was this. See [rounding](#rounding).
### halt
@@ -295,11 +314,11 @@ The onchain function that processes an instruction. This book always says "instr
### insurance fund
-Capital a venue sets aside to absorb losses larger than the collateral behind them, before those losses reach the liquidity providers. It does not make a venue lose less money; it names, before the loss happens, who pays for it. The tier above it is [auto-deleveraging](#auto-deleveraging-adl). See the *Perpetual Futures* chapter.
+Capital a venue sets aside to absorb losses larger than the collateral behind them, before those losses reach the liquidity providers. It does not make a venue's losses smaller; it names, before the loss happens, who pays for it. The tier above it is [auto-deleveraging](#auto-deleveraging-adl). See the *Perpetual Futures* chapter.
### invariant
-A property that must hold before and after every state change: K must not decrease, the vault equals what the book owes, total in equals total out. Asserted in code after the math, not just assumed from it. An **invariant test** asserts one against generated inputs, and a model checker proves one over every possible input; the *Operating Financial Software* chapter sets how far to go by how much money the program will hold.
+A condition that must hold before and after every state change: a pool's [K](#k) must not decrease, the vault equals what the book owes, total in equals total out. Asserted in code after the math, not just assumed from it. An **invariant test** asserts one against generated values, and a model checker checks one against every possible value; the *Operating Financial Software* chapter sets how far to go by the value of the assets the program will hold.
### inventory
@@ -379,7 +398,7 @@ The maker posted the resting order; the taker crossed the spread to hit it. Fill
### managed fund
-The onchain equivalent of a mutual fund: pooled deposits, a mandate fixing which assets may be held, a [manager](#manager) who sets the allocation, and shares priced against [NAV](#nav-net-asset-value). Several Solana platforms call it a [vault strategy](#vault-strategy). Distinct from a [vault](#vault), which in this book is only ever the token account the money sits in. See the *Managed Fund* chapter.
+The onchain equivalent of a mutual fund: pooled deposits, a mandate fixing which assets may be held, a [manager](#manager) who sets the allocation, and shares priced against [NAV](#nav-net-asset-value). Several Solana platforms call it a [vault strategy](#vault-strategy). Distinct from a [vault](#vault), which in this book is only ever the token account the deposits sit in. See the *Managed Fund* chapter.
### manager
@@ -395,7 +414,7 @@ On a perpetual futures market, the index price is the oracle's statement of the
### mark to market
-Valuing a position or pool at current prices rather than at the prices it was entered at. Health checks, liquidation gates, and LP share pricing (see [AUM](#aum-assets-under-management)) all mark to market, because money moves on today's valuation.
+Valuing a position or pool at current prices rather than at the prices it was entered at. Health checks, liquidation checks, and LP share pricing (see [AUM](#aum-assets-under-management)) all mark to market, because tokens change hands at today's prices.
### market maker
@@ -419,7 +438,7 @@ The onchain account that defines a type of token: its supply, decimals, and auth
### multisig
-An account that records a set of owners and how many must agree before it acts, used wherever one person holding a key is too much trust: a mint authority, a market's fee authority, a program's upgrade authority. A transaction can carry several signatures natively; a multisig is that idea made into standing policy.
+An account that records a set of owners and how many must agree before it acts, used wherever one person holding a private key is too much trust: a mint authority, a market's fee authority, a program's upgrade authority. A transaction can carry several signatures natively; a multisig is that idea made into standing policy.
### NAV (net asset value)
@@ -441,6 +460,10 @@ A borrower's loan file in a lending market: their collateral deposits, their bor
The total notional of positions currently open on a market, usually tracked per side. A perpetual futures venue's funding direction and solvency reasoning both read it.
+### open source
+
+Software released under a license that meets the [Open Source Definition](https://opensource.org/osd), which requires, among other things, that anyone may read, modify and redistribute the source code. Publishing the source is not enough on its own: code whose license forbids modifying or redistributing it is not open source.
+
### option
The right, but not the obligation, to buy ([call](#call)) or sell ([put](#put)) an asset at a fixed price (the [strike](#strike)) before a fixed date (the [expiry](#expiry)), bought from a [writer](#writer) for a [premium](#premium). The *Options* chapter builds a venue for them.
@@ -559,7 +582,7 @@ The URL a program or wallet sends its transactions to: a server holding a [clust
### RWA (real-world asset)
-A traditional asset represented as a token onchain: commodities, equities, real estate, and exotics. The stocks this book's markets trade (NVDAx, TSLAx, SPCXx) are RWAs. As of mid-2026, Solana is the most popular chain for RWA trading.
+A traditional [asset](#asset) represented as a [token](#token) onchain: commodities, equities, real estate, and exotics. The stocks this book's markets trade (NVDAx, TSLAx, SPCXx) are RWAs. As of mid-2026, Solana is the most popular chain for RWA trading. "RWA" lumps very different things together, so it is usually clearer to name the asset class instead: tokenized stocks, tokenized Treasury bills, gold.
### sandwich
@@ -567,11 +590,11 @@ Wrapping someone's pending trade in two of your own: buy ahead of it to move the
### saturating
-What Rust's `saturating_add` and `saturating_mul` do when a result will not fit: clamp to the type's maximum or minimum and continue. Less silent than [wrapping](#wrapping) and still wrong on a balance, because the number returned is not the answer. Money uses [checked arithmetic](#checked-arithmetic).
+What Rust's `saturating_add` and `saturating_mul` do when a result will not fit: clamp to the type's maximum or minimum and continue. Less silent than [wrapping](#wrapping) and still wrong on a balance, because the number returned is not the answer. Balances use [checked arithmetic](#checked-arithmetic).
### settlement
-Paying out what a contract finally owes, and closing it. A betting market settles when the authority records the winning outcome and the losing pool is divided among the winners; a position settles when it closes and its [PnL](#pnl-profit-and-loss) becomes real balances. Before settlement the numbers are marks, not money.
+Paying out what a contract finally owes, and closing it. A betting market settles when the authority records the winning outcome and the losing pool is divided among the winners; a position settles when it closes and its [PnL](#pnl-profit-and-loss) becomes real balances. Before settlement the numbers are marks, not balances.
### share
@@ -583,7 +606,7 @@ An account whose private key signed the transaction. "Is this account a signer"
### significand
-The digits of a number stated separately from its scale: a price feed holding significand 25,000,000 and exponent −5 means $250. Storing the two as integers is how oracle prices stay exact onchain; floating point packs the same two parts into one machine word, with the representation error that keeps floats away from money (see the *Arithmetic of Money* chapter). "Significand" is the preferred term because "[mantissa](#mantissa)" has a different meaning in mathematics (the fractional part of a logarithm), but some oracle operators say "mantissa" for the digits, with exactly the meaning given here.
+The digits of a number stated separately from its scale: a price feed holding significand 25,000,000 and exponent −5 means $250. Storing the two as integers is how oracle prices stay exact onchain; floating point packs the same two parts into one machine word, with the representation error that keeps floats away from token amounts and prices (see the *Arithmetic of Money* chapter). "Significand" is the preferred term because "[mantissa](#mantissa)" has a different meaning in mathematics (the fractional part of a logarithm), but some oracle operators say "mantissa" for the digits, with exactly the meaning given here.
### size ladder
@@ -601,6 +624,10 @@ Solana's basic unit of time: a window in which one [leader](#leader) may produce
Solana's **native token**: the asset the network understands without any token program, used to pay transaction fees and to fund the rent deposit on every account. Denominated in [lamports](#lamport), a billionth of a SOL each. Every other token is an [SPL token](#spl-token).
+### Solana
+
+A public blockchain: a network of validators that run programs and keep a shared record of every account. Programs on it hold and move [tokens](#token), and transactions settle in under a second for a fraction of a cent. Solana is not an asset: its native token, [SOL](#sol), is the asset. Nor is Solana an organization. Many organizations work on Solana.
+
### SPL token
Any token other than [SOL](#sol): one defined by a token program rather than by the network itself. The name comes from the Solana Program Library the original token program shipped in. Most people just say "token", and so does this book.
@@ -641,6 +668,10 @@ How long an [option](#option) runs: the time from the trade that creates it to i
The minimum price increment of a market. Prices must land on a multiple of it, which keeps the book from fragmenting into dust price levels.
+### token
+
+A unit of an [asset](#asset) on Solana. [SOL](#sol) is Solana's **native token**, used to pay transaction fees, and every account holds it directly as [lamports](#lamport). All other tokens, such as USDC and NVDAx, are [SPL tokens](#spl-token): each type is defined by a [mint](#mint-account), and a wallet's balance of it is held in a [token account](#token-account). Two tokens from the same mint are the same asset; tokens from different mints are different assets, whatever their names and symbols say. Amounts are integers in [minor units](#minor-unit).
+
### token account
The account holding a wallet's balance of one specific token. Owned by the token program; controlled by its recorded authority.
@@ -687,11 +718,11 @@ A fixed-point convention: values scaled by 10^18, so 1.0 is stored as a 1 follow
### wallet
-A key pair used as an identity for holding and moving assets, with the private key stored safely. On disk, a key pair file is just 64 bytes; whoever holds them is the wallet. **Wallet software** is the tool that holds those keys, signs transactions with them, and shows the balances they control, whether a phone or desktop app or browser extension.
+A key pair used as an identity for holding and moving assets, with the private key stored safely. On disk, a key pair file is just 64 bytes; whoever holds them is the wallet. **Wallet software** is the tool that holds the private keys, signs transactions with them, and shows the balances they control, whether a phone or desktop app or browser extension.
### wrapping
-What a plain `+` or `*` does in a Rust release build when the result is too big for the type: it starts again from zero, so a `u64` one past the maximum becomes almost nothing. Silent, and catastrophic on a balance, which is why money uses [checked arithmetic](#checked-arithmetic) instead.
+What a plain `+` or `*` does in a Rust release build when the result is too big for the type: it starts again from zero, so a `u64` one past the maximum becomes almost nothing. Silent, and catastrophic on a balance, which is why every amount in this book uses [checked arithmetic](#checked-arithmetic) instead.
### writer
diff --git a/building-financial-products-in-solana/betting-market.md b/building-financial-products-in-solana/betting-market.md
index 338da944..c6103768 100644
--- a/building-financial-products-in-solana/betting-market.md
+++ b/building-financial-products-in-solana/betting-market.md
@@ -1,12 +1,12 @@
# Chapter 13: The Betting Market
-What is going to be the top-grossing film this month? You have an opinion, and so does everyone else. This chapter builds the program that lets everyone put money on it: a **parimutuel** betting market, the kind with no bookmaker. Nobody sets odds. The odds *are* the pool: everyone's stakes go into one vault, and when the event resolves, the winners split the losers' money in proportion to how much they staked. Racetracks have run on this model since the 1870s, because the house never takes a position; it just takes a fee.
+What is going to be the top-grossing film this month? You have an opinion, and so does everyone else. This chapter builds the program that lets everyone put money on it: a **parimutuel** betting market, the kind with no bookmaker. Nobody sets odds. The odds *are* the pool: everyone's stakes go into one vault, and when the event resolves, the winners split the losers' stakes in proportion to how much they staked. Racetracks have run on this model since the 1870s, because the house never takes a position; it just takes a fee.
The program lives at `finance/betting-market` in the companion repository. Structurally it is the *Escrow* chapter with many parties and one new, serious question: who decides what happened in the real world?
## Nobody Sets the Odds: The Pool Is the Odds
-In a bookmaker market, the house quotes odds and carries risk on every bet. In a parimutuel market, the program carries no risk. All stakes on all outcomes pool together. When the event settles, the losing side's money (minus the house fee) is distributed to the winning side, pro rata by stake. Your payout odds are the ratio between the pools at settlement time, which means the odds float as people bet, and betting heavily on a favorite compresses everyone's payout.
+In a bookmaker market, the house quotes odds and carries risk on every bet. In a parimutuel market, the program carries no risk. All stakes on all outcomes pool together. When the event settles, the losing side's stakes (minus the house fee) are distributed to the winning side, pro rata by stake. Your payout odds are the ratio between the pools at settlement time, which means the odds float as people bet, and betting heavily on a favorite compresses everyone's payout.
**Parimutuel markets reward conviction.** Bet three times as much as the next winner and you win three times as much of the losing pool.
@@ -14,7 +14,7 @@ In a bookmaker market, the house quotes odds and carries risk on every bet. In a
## The Operator Creates the Market
-Maria is the admin. Her key is recorded in the program's config, and only she can create events, add outcomes, open betting, settle, and cancel; her revenue is a 2% fee, charged only on the losing pool. She runs `initialize_config()` once, naming herself admin, USDC as the token, herself as fee recipient, and 200 basis points (2%) as the fee. For this month's market she runs `initialize_event()` with a description ("top-grossing film this month") and a `betting_closes_at` of midnight on the last day of the month. A close time already in the past fails with `CloseTimeInPast`. The event starts as a Draft, and Maria calls `add_outcome()` twice: "Toy Story 5" and "Backrooms". Then she calls `open_betting()`, which moves the event to Open. Every one of these instruction handlers checks her signature against the admin address stored in the config and rejects anyone else with `Unauthorized`; the test `test_only_admin_can_initialize_event` checks it.
+Maria is the admin. Her public key is recorded in the program's config, and only she can create events, add outcomes, open betting, settle, and cancel; her revenue is a 2% fee, charged only on the losing pool. She runs `initialize_config()` once, naming herself admin, USDC as the token, herself as fee recipient, and 200 basis points (2%) as the fee. For this month's market she runs `initialize_event()` with a description ("top-grossing film this month") and a `betting_closes_at` of midnight on the last day of the month. A close time already in the past fails with `CloseTimeInPast`. The event starts as a Draft, and Maria calls `add_outcome()` twice: "Toy Story 5" and "Backrooms". Then she calls `open_betting()`, which moves the event to Open. Every one of these instruction handlers checks her signature against the admin address stored in the config and rejects anyone else with `Unauthorized`; the test `test_only_admin_can_initialize_event` checks it.

@@ -29,15 +29,15 @@ The **event** is an account at a PDA (seeds: `"event"`, the event ID) with a des
Each **outcome** is an account at a PDA (seeds: `"outcome"`, the event, an index byte) holding its label, its `total_amount` staked, and its `bet_count`.
-One rule protects the bettors: **the outcome list is fixed before any money can arrive**. `add_outcome()` works only on a Draft and fails with `EventNotDraft` once the event is Open. `place_bet()` on a Draft fails with `EventNotOpen`, so nobody can bet while the list is still changing. `open_betting()` refuses a market with fewer than two outcomes (`NotEnoughOutcomes`), because a single outcome has no losing side to pay its winners from. The tests `test_outcomes_lock_when_betting_opens` and `test_open_betting_needs_two_outcomes` check all three rules.
+One rule protects the bettors: **the outcome list is fixed before any stake can arrive**. `add_outcome()` works only on a Draft and fails with `EventNotDraft` once the event is Open. `place_bet()` on a Draft fails with `EventNotOpen`, so nobody can bet while the list is still changing. `open_betting()` refuses a market with fewer than two outcomes (`NotEnoughOutcomes`), because a single outcome has no losing side to pay its winners from. The tests `test_outcomes_lock_when_betting_opens` and `test_open_betting_needs_two_outcomes` check all three rules.
-The Draft status is what makes the rule hold. The simpler design locks the list at the first bet, by refusing `add_outcome()` once the pool holds any money. That lets anyone freeze a half-built market with a one-minor-unit bet on its only outcome. It also lets Maria's last `add_outcome()` and the first bet land in either order, so the first bettor can stake on a list with an outcome their screen never showed. An explicit status that only Maria can change has neither problem.
+Outcomes can be added only while the event is a Draft, and only Maria can move it to Open, so the list is fixed at the moment she opens betting. A simpler but naive design would lock the list at the first bet, by refusing `add_outcome()` once the pool holds any stakes. That would let anyone freeze a half-built market with a one-minor-unit bet on its only outcome. It would also let Maria's last `add_outcome()` and the first bet land in either order, so the first bettor could stake on a list with an outcome their screen never showed. An explicit status that only Maria can change has neither problem.
## Bettors Stake on Their Chosen Outcomes
Three bettors take a view, each from the standard wallet of one SOL and 1,000 USDC, and all in USDC, the token the config designates. Alice is confident about school holidays and puts $300 on Toy Story 5. Bob agrees but has less to spend: $100 on Toy Story 5. Carol believes in word of mouth and puts $200 on Backrooms.
-Each bettor calls `place_bet()` with an amount, any time before the close. Zero-amount bets reject with `ZeroAmount`, and a bet once the clock reaches `betting_closes_at` rejects with `BettingClosed`. The handler moves the stake immediately from the bettor's token account into the event's vault (the bettor signs this transfer directly; no PDA signature is needed on the way in). It then creates the bet account if this is the bettor's first bet on this outcome, and bumps the outcome's `total_amount` and the event's `total_pool`. A fresh bet account has amount zero, which is how the handler tells a first bet from a top-up and gates the per-outcome bookkeeping.
+Each bettor calls `place_bet()` with an amount, any time before the close. Zero-amount bets reject with `ZeroAmount`, and a bet once the clock reaches `betting_closes_at` rejects with `BettingClosed`. The handler moves the stake immediately from the bettor's token account into the event's vault (the bettor signs this transfer directly; no PDA signature is needed on the way in). It then creates the bet account if this is the bettor's first bet on this outcome, and bumps the outcome's `total_amount` and the event's `total_pool`. A fresh bet account has amount zero, which is how the handler tells a first bet from a top-up: only a first bet adds one to the outcome's `bet_count` and adds the bet to the bettor's list of open bets.
After all three bets, the vault holds 600 USDC: 400 staked on Toy Story 5, 200 on Backrooms.
@@ -47,7 +47,7 @@ A bettor's first bet also created two accounts for them. Each **bet** is an acco
## The Film Grosses: The Operator Settles the Event
-The month ends, and betting closes at midnight. In the real world, Toy Story 5 wins. In program terms, the fact that decides where 600 USDC goes exists entirely offchain. This program resolves it by having a designated trusted party sign. Maria calls `settle_event()` with the winning outcome's index, her signature is checked against the config's admin, and her word becomes the onchain truth. The *Offchain Truth* chapter covers the full spectrum, from a single admin key like this one, through multi-party resolution, to signed price feeds. This program makes the trusted party explicit and limits what their signature can do. Maria can pick which outcome won, but she cannot touch the pro-rata math, take more than the snapshotted fee, or reach into the vault by any other path.
+The month ends, and betting closes at midnight. In the real world, Toy Story 5 wins. In program terms, the fact that decides where 600 USDC goes exists entirely offchain. This program resolves it by having a designated trusted party sign. Maria calls `settle_event()` with the winning outcome's index, her signature is checked against the config's admin, and her answer is written to the event account as `winning_outcome_index`. The *Offchain Truth* chapter covers the full spectrum, from a single admin signer like this one, through multi-party resolution, to signed price feeds. This program makes the trusted party explicit and limits what their signature can do. Maria can pick which outcome won, but she cannot touch the pro-rata math, take more than the snapshotted fee, or reach into the vault by any other path.
`settle_event()` works only once the clock reaches `betting_closes_at`, and earlier fails with `BettingStillOpen`. The two windows split time exactly: a bet needs the clock before the close, and settlement needs it at or after. So no bet can land once the event can be settled, and Maria cannot end the market early (checked by `test_betting_closes_at_close_time`).
@@ -62,28 +62,28 @@ Settlement does four things, none of which involve paying bettors yet:
## Winners Claim: Conviction Pays Three to One
-Winners come and get their money whenever they like; nothing pays out automatically. Alice calls `claim_winnings()`. Her payout is her stake back plus her pro-rata share of the distributable losing pool:
+Winners come and claim their payouts whenever they like; nothing pays out automatically. Alice calls `claim_winnings()`. Her payout is her stake back plus her pro-rata share of the distributable losing pool:
payout = stake + floor(stake × distributable_losing_pool ÷ winning_pool)
For Alice: 300 + floor(300 × 196 ÷ 400) = 300 + 147 = **447 USDC**. For Bob: 100 + 49 = **149 USDC**. Alice staked three times what Bob staked and won exactly three times the winnings. Carol staked 200 and receives nothing; had Backrooms won, she alone would have taken the entire 400-dollar losing side, minus the fee.
-The multiply runs in u128 and divides exactly once, after the multiplication, so the result is floored a single time; dividing first would throw away precision on every claim. The floor means at most a few minor units of dust can remain in the vault, and dust belongs to the house, not to whichever claimant happened to arrive last. This is the multiply-before-divide rule and the rounding-policy rule from the *Arithmetic of Money* chapter, in six lines.
+The multiply runs in u128 and divides exactly once, after the multiplication, so the result is floored a single time; dividing first would throw away precision on every claim. The floor means at most a few minor units of dust can remain in the vault, and dust belongs to the house, not to whichever claimant happened to arrive last. The *Arithmetic of Money* chapter explains both choices: multiply before dividing, and round so that the leftover dust stays with the program rather than going to a claimant.
Two orderings in this handler close attacks:
- **The bet is removed from the user's index before the token transfer.** Effects before interactions, so no re-entry path can see stale state.
- **The bet account itself closes at the end of the claim**, rent refunded to the bettor, and **a closed account is the double-claim defense**: Alice claiming twice fails because the bet account the second claim needs is already gone (checked by `test_double_claim_fails`).
-There is no `claimed` boolean to forget to check, because the account's deletion is the exit.
+There is no `claimed` boolean to forget to check.

-## Closing Out, and an Exit From Every State
+## Closing Out, and Getting Assets Out of Every State
Carol's losing bet pays nothing, but it still occupies one of her 32 position slots and holds rent. She calls `close_losing_bet()`, which checks the event is settled and her outcome is not the winner (a winner trying to use this cheaper path rejects with `BetWon` and must use `claim_winnings()`), then closes the account and frees the slot. Both rules are checked by `test_close_losing_bet_only_after_settle_and_only_for_losers` and `test_closing_a_bet_frees_a_slot_for_a_new_bet`.
-And what if the film festival burns down? An Open event has a second exit: `cancel_event()`, admin-only, flips the status to Cancelled, and every bettor reclaims their exact stake through `claim_refund()`, no fee taken, using the same close-the-account-to-prevent-double-claims mechanic (checked by `test_cancel_and_refund`). `cancel_event()` works on a Draft too, which is how Maria abandons a market she set up wrong before anyone has money in it. Draft, Open, Settled, Cancelled: four states, and every one of them has an exit for every participant's money.
+And what if the film festival burns down? An Open event can also end another way: `cancel_event()`, admin-only, flips the status to Cancelled, and every bettor reclaims their exact stake through `claim_refund()`, no fee taken, using the same close-the-account-to-prevent-double-claims mechanic (checked by `test_cancel_and_refund`). `cancel_event()` works on a Draft too, which is how Maria abandons a market she set up wrong before anyone has staked on it. Draft, Open, Settled, Cancelled: four states, and in each one every bet account has a handler that pays it out or closes it.

@@ -96,9 +96,9 @@ The financial arithmetic of this program is four expressions:
- **Each winner's share**: a u128 multiply of stake by distributable pool, then one floor division by the winning pool.
- **The payout**: stake plus share, checked.
-Every checked operation maps to `MathOverflow`. There are no rates and no curves, and time appears only as one comparison against the close; the betting market shows the arithmetic rules with nothing else in the way.
+Every checked operation maps to `MathOverflow`. There are no rates and no curves, and time appears only as one comparison against the close.
-## The Invariants: The Vault Equals the Promises
+## The Invariants: The Vault Covers Every Claim
- At every moment, the vault balance equals stakes not yet claimed: before settlement, the total pool; after settlement, winning stakes plus unclaimed winnings plus dust.
- The winning pool, losing pool, and fee always sum exactly to the pre-settlement total pool.
@@ -109,17 +109,17 @@ Every checked operation maps to `MathOverflow`. There are no rates and no curves
## The Failure Modes: The Oracle Is a Person, and Everything Downstream of That
-- **The resolution itself.** Maria could settle the wrong outcome, maliciously or by mistake. The program limits the damage to that one decision (she cannot skim beyond the fee or redirect funds), but the decision is hers alone. This program is deliberately the simplest case; the *Offchain Truth* chapter covers what it costs a trusted party to lie, and how to grow out of a single key.
+- **The resolution itself.** Maria could settle the wrong outcome, maliciously or by mistake. The program limits the damage to that one decision (she cannot skim beyond the fee or redirect funds), but the decision is hers alone. This program is deliberately the simplest case; the *Offchain Truth* chapter covers what it costs a trusted party to lie, and how to grow out of a single signer.
- **Settling an empty outcome** would divide by zero in every claim; rejected at the source with `OutcomeHasNoBets`.
- **Double claims** fail because the bet account is gone. There is no flag to desynchronize.
- **Changing the game mid-play**: `open_betting()` fixes the outcome list before any bet can land, and the fee and close time are fixed per event at creation.
- **Betting on a known result**: a bettor who learns the result before Maria settles cannot stake on it, because `place_bet()` stops at `betting_closes_at` with `BettingClosed`. The program cannot tell when the result becomes public, so choosing a close time before that moment is Maria's job.
- **Slot exhaustion**: 32 concurrent positions per user, `TooManyBets` beyond it, and `close_losing_bet()` to recycle slots.
-- **Betting after settlement or cancellation** rejects with `EventNotOpen`, whichever exit the event took.
+- **Betting after settlement or cancellation** rejects with `EventNotOpen`, whether the event was settled or canceled.
## Testing Considerations
-The suite builds all state through the program's own handlers, and its centerpiece is `test_full_lifecycle`: the exact story above, config through claims, with balances asserted at each step. Around it, one test per failure mode: admin gating, the outcome list locking when betting opens, the two-outcome minimum, both edges of the close time, a close time in the past, bet-after-settle, double claims, empty-outcome settlement, cancel-and-refund, and both rules of losing-bet closure. When you write your own market, copy the shape: the happy path proves the math, and every adversarial code path gets a test whose name says what it refuses to allow.
+The suite builds all state through the program's own handlers, and its centerpiece is `test_full_lifecycle`: the exact story above, config through claims, with balances asserted at each step. Around it, one test per failure mode: the admin-only handlers, the outcome list locking when betting opens, the two-outcome minimum, both edges of the close time, a close time in the past, bet-after-settle, double claims, empty-outcome settlement, and cancel-and-refund. `test_close_losing_bet_only_after_settle_and_only_for_losers` checks that `close_losing_bet()` works only on a settled event and only for a bet that lost. When you write your own market, copy the shape: the happy path checks the math, and every adversarial code path gets a test whose name says what it refuses to allow.
## What You Learnt from Building the Betting Market
@@ -128,9 +128,9 @@ A racetrack's parimutuel pool needs a tote board, a licensed operator, and a pay
This chapter added five concepts:
- **The parimutuel structure.** All stakes pool together; winners split the losing pool pro rata by stake, so conviction pays proportionally and no one sets odds.
-- **Offchain truth by designated signer.** The operator's signature is the oracle, and the program's job is making that trust visible and narrow: they pick the winner, but cannot touch the math, the snapshotted fee, or the vault by any other path.
-- **Terms locked when money commits.** An explicit Draft status keeps bets out until the admin opens the market, so the outcome list is fixed before the first stake. The fee and close time are fixed at event creation, so the question bettors staked on cannot be rewritten.
-- **Account closure as the double-claim defense.** A claimed bet's account is gone, so there is no flag to desynchronize; the state machine's exit is the account's deletion.
-- **An exit from every state.** Draft and Open events can be canceled, and stakes in a canceled event are refunded in full, so a burned-down film festival strands nobody's stake.
+- **A designated signer decides the result.** The operator acts as the oracle, and the program's job is making that trust visible and narrow: they pick the winner, but cannot touch the math, the snapshotted fee, or the vault by any other path.
+- **Terms locked before bettors commit stakes.** An explicit Draft status keeps bets out until the admin opens the market, so the outcome list is fixed before the first stake. The fee and close time are fixed at event creation, so the question bettors staked on cannot be rewritten.
+- **Account closure as the double-claim defense.** A claimed bet's account is gone, so there is no flag to desynchronize.
+- **Cancellation refunds every stake.** Draft and Open events can be canceled, and stakes in a canceled event are refunded in full, so a burned-down film festival strands nobody's stake.
-In the *Order Book Exchange* chapter we will build an exchange. Instead of paying into one pool, traders post orders at the prices they want, backed by tokens locked when the order is placed, and the program matches each new order against the best resting one. You will see how orders are stored, matched, and paid out onchain, where every step costs compute. The pooled payout formula and the trusted signer both return later, in the *Lending and Borrowing* and *Perpetual Futures* chapters, where the trusted signature is a price feed and settlement runs continuously instead of once.
+In the *Order Book Exchange* chapter we will build an exchange. Instead of paying into one pool, traders post orders at the prices they want, backed by tokens moved into a vault when the order is placed, and the program matches each new order against the best resting one. You will see how orders are stored, matched, and paid out onchain, where every step costs compute. The pooled payout formula and the trusted signer both return later, in the *Lending and Borrowing* and *Perpetual Futures* chapters, where the trusted signature is a price feed and settlement runs continuously instead of once.
diff --git a/building-financial-products-in-solana/constant-product-amm.md b/building-financial-products-in-solana/constant-product-amm.md
index fb5cb62d..cc0965c1 100644
--- a/building-financial-products-in-solana/constant-product-amm.md
+++ b/building-financial-products-in-solana/constant-product-amm.md
@@ -39,7 +39,7 @@ The **pool config** is an account at a PDA seeded by the config and the two mint
The tokens are held in two vaults, `pool_a` and `pool_b`, whose authority is the pool config PDA, alongside the **LP mint** (`liquidity_provider_mint`), whose tokens are the receipts liquidity providers hold and whose mint authority is the same PDA. Only the program can sign as that PDA, with the pool config's seeds, so nobody else can move the vaults' tokens or mint LP tokens.
-Two more fields are `admin_fees_owed_a` and `admin_fees_owed_b` on the pool config. The admin's share of fees is never transferred during a swap; it accrues in these counters as a *virtual claim* while the tokens physically sit in the vaults. So every piece of LP-facing math runs on **effective reserves**: the raw vault balance minus the admin's owed slice. The admin's fees do not boost LP yield, do not distort the price, and get swept out later in one transfer. Keeping who is owed what separate from where it sits is the order book's fee vault, done with accounting instead of an account.
+Two more fields are `admin_fees_owed_a` and `admin_fees_owed_b` on the pool config. The admin's share of fees is never transferred during a swap; it accrues in these counters as a *virtual claim* while the tokens physically sit in the vaults. So every piece of LP-facing math runs on **effective reserves**: the raw vault balance minus the admin's owed slice. The admin's fees do not boost LP yield, do not distort the price, and get swept out later in one transfer. The order book kept its fees in a separate fee vault; this pool leaves them in the trading vaults and records the admin's share in these two counters.
Empty pools cannot price anything, so Maria seeds it. She believes a fair launch price is $2.25, so she runs `deposit_liquidity()` with her 400 ACME and 900 USDC, the founder stock and nine-tenths of her cash. **The first deposit sets the price**, because the ratio she deposits *is* the ratio. X = 400, Y = 900, K = 360,000.
@@ -51,13 +51,13 @@ Her LP tokens come from the formula the *Arithmetic of Money* chapter described:
## Adding Liquidity in Ratio Does Not Move the Price
-Alice works at ACME, holds a grant of 100 ACME alongside her standard wallet, and becomes the second liquidity provider: she wants to earn fees on her ACME grant. Liquidity providers are not allowed to change the pool's ratio, so her 100 ACME must arrive with a matching 225 USDC (100 × $2.25). The handler enforces this by clamping: it computes how much of token B her token A implies at the current effective reserves, uses whichever pairing fits inside what she offered, and rejects with `DepositAmountTooSmall` if the clamp rounds her out entirely. Her LP tokens are minted pro rata, the minimum of her share against each side, floored, and guarded by her `minimum_lp_tokens_out`. Her share is measured against the LP supply plus `MINIMUM_LIQUIDITY`, because the floor is part of the supply the reserves are divided among.
+Alice works at ACME, holds a grant of 100 ACME alongside her standard wallet, and becomes the second liquidity provider: she wants to earn fees on her ACME grant. Liquidity providers are not allowed to change the pool's ratio, so her 100 ACME must arrive with a matching 225 USDC (100 × $2.25). The handler enforces this by clamping: it computes how much of token B her token A implies at the current effective reserves, uses whichever pairing fits inside what she offered, and rejects with `DepositAmountTooSmall` if the clamp rounds her out entirely. Her LP tokens are minted pro rata, the minimum of her share against each side, floored, and checked against her `minimum_lp_tokens_out`. Her share is measured against the LP supply plus `MINIMUM_LIQUIDITY`, because the floor is part of the supply the reserves are divided among.
X is now 500, Y is 1,125, K is 562,500. The price is still exactly $2.25. **Deposits grow K without touching the price**; that is the first of K's two ways of growing.
## A Buy Moves the Price Along the Curve
-Bob is early and bullish. He runs `swap_tokens()` to spend 100 USDC on ACME, a tenth of everything he has, with a `min_output_amount` as his slippage guard. Three things happen in strict order.
+Bob is early and bullish. He runs `swap_tokens()` to spend 100 USDC on ACME, a tenth of everything he has, with a `min_output_amount` as his slippage check. Three things happen in strict order.
**The fee is deducted first.** 30 basis points of 100 is 30 cents. The admin share, about 5 cents (1,667 basis points of it, floored in minor units), accrues to `admin_fees_owed_b` as Maria's virtual claim; the remaining 25 cents stays in the pool, where it adds to the reserves that back every LP token. Bob's taxed input is 99.70.
@@ -85,7 +85,7 @@ Whenever she likes, Maria runs `claim_admin_fees()`. It zeroes both owed counter
Alice wants her capital back. `withdraw_liquidity()` burns her LP tokens and pays her a pro-rata slice of both effective reserves, dividing by the LP supply *plus* `MINIMUM_LIQUIDITY`, the same divisor her deposit was minted against. The locked floor is backed by reserves but absent from the supply counter. Leaving it out here would overpay every withdrawer, and leaving it out of the deposit would underpay every depositor.
-Alice does not get back what she deposited. She deposited 100 ACME and 225 USDC; the pool's mix has shifted since, so her slice is about 92.3 ACME and 244 USDC. Valued at the current price, that is roughly $1.50 *less* than if she had simply held her original tokens in her wallet: a small number at launch scale, and the same number becomes serious money once the pool has six more zeroes on it.
+Alice does not get back what she deposited. She deposited 100 ACME and 225 USDC; the pool's mix has shifted since, so her slice is about 92.3 ACME and 244 USDC. Valued at the current price, that is roughly $1.50 *less* than if she had simply held her original tokens in her wallet: a small number at launch scale, and the same number becomes a serious loss once the pool has six more zeroes on it.
That gap is **impermanent loss**, and it is the price of being an LP. A constant-product pool mechanically sells the token that is rising and accumulates the token that is falling, so an LP always slightly underperforms just holding, whenever the price moves in either direction. Fees are the compensation. **An LP is a bet on trading volume, not on price direction**: if accumulated fees outrun the divergence, LPing wins; if the price runs hard in one direction on thin volume, holding wins. It is called *impermanent* because it only crystallizes at withdrawal; had Alice stayed in and ACME returned to $2.25, the loss would have evaporated.
@@ -117,27 +117,27 @@ Every division in this program floors. Swap output, LP minting, and withdrawal a
## The Failure Modes: Empty Pools, First Deposits, and the Vacuous Invariant
-- **The empty-reserve drain.** If one reserve were zero, the swap formula outputs the *entire* other reserve (output = input × other ÷ (0 + input) = other). The invariant check would not catch it either, because the pre-trade K would be 0 × other = 0, and "new K ≥ 0" holds vacuously. A guard requires both effective reserves positive (`EmptyPoolReserve`) before any pricing runs.
+- **The empty-reserve drain.** If one reserve were zero, the swap formula outputs the *entire* other reserve (output = input × other ÷ (0 + input) = other). The invariant check would not catch it either, because the pre-trade K would be 0 × other = 0, and "new K ≥ 0" holds vacuously. A check requires both effective reserves positive (`EmptyPoolReserve`) before any pricing runs.
- **The inflation attack.** The empty-pool attack from the *Adversary* chapter: a first depositor mints dust-level LP supply, donates tokens straight to the vaults to inflate the value behind each LP token, and waits for a later deposit to round down to zero. The `MINIMUM_LIQUIDITY` floor makes the attack uneconomic, because `deposit_liquidity()` and `withdraw_liquidity()` both divide by the LP supply plus the floor. The smallest pool an attacker can open leaves them 1 LP token out of 101 units of supply. Rounding a deposit down to zero then takes a donation of about 101 times that deposit, and 100/101 of the donation belongs to units nobody can redeem. The floor has to be in both divisors: a deposit divided by the bare supply of 1 rounds down against a donation merely as large as itself. `test_donation_cannot_round_a_later_deposit_to_zero` runs the attack with a donation of 1,000,000 minor units to each vault: the victim's equal deposit is minted 100 LP tokens, and the attacker's 1 LP token redeems 9,950 minor units of each token.
- **The first deposit sets the price, and anyone can be first.** The program's own source notes that pool creation can be front-run with a bad ratio. The floor bounds the share-math games, but a launch operator should still expect arbitrage traffic (or hostile seeding) until the pool's price matches the market's, and seed with size.
-- **Slippage and sandwiches.** Every trade and deposit carries a caller-supplied minimum (`min_output_amount`, `minimum_lp_tokens_out`, the withdrawal minimums), and the tests prove each one rejects (`SlippageExceeded`, `DepositBelowMinimum`, `WithdrawalBelowMinimum`). What the program cannot do is choose the number for you; a zero minimum is legal and is what a sandwich attacker hopes you send.
-- **Liquidity fragmentation.** Canonical mint ordering, one pool per pair, by construction.
+- **Slippage and sandwiches.** Every trade and deposit carries a caller-supplied minimum (`min_output_amount`, `minimum_lp_tokens_out`, the withdrawal minimums), and the tests check that each one rejects (`SlippageExceeded`, `DepositBelowMinimum`, `WithdrawalBelowMinimum`). What the program cannot do is choose the number for you; a zero minimum is legal and is what a sandwich attacker hopes you send.
+- **Liquidity fragmentation.** Because `initialize_pool()` fails with `InvalidMintOrder` unless `mint_a` sorts below `mint_b`, each pair of tokens can have only one pool, so its liquidity is never split between two.
## Testing Considerations
-The suite (`tests/test_swap.rs`) covers each section of this chapter. Two tests, `test_invariant_holds_after_normal_swap` and `test_invariant_holds_after_b_to_a_swap`, check K's direction both ways, while `test_lp_mint_after_swap_uses_effective_reserves` and `test_deposit_after_swap_uses_shifted_effective_ratio` check the admin-fee exclusion, the accounting a refactor is most likely to break. Two more, `test_deposit_clamps_excess_amount_a` and `_b`, check the ratio clamp from both directions (both directions of every symmetric flow), and the three reverts-below-minimum tests check every slippage guard. If you write one invariant test for your own AMM, make it the round trip: swap in, swap back, and assert the pool did not lose value.
+The suite (`tests/test_swap.rs`) covers each section of this chapter. Two tests, `test_invariant_holds_after_normal_swap` and `test_invariant_holds_after_b_to_a_swap`, check K's direction both ways, while `test_lp_mint_after_swap_uses_effective_reserves` and `test_deposit_after_swap_uses_shifted_effective_ratio` check the admin-fee exclusion, the accounting a refactor is most likely to break. Two more, `test_deposit_clamps_excess_amount_a` and `_b`, check the ratio clamp from both directions (both directions of every symmetric flow), and the three reverts-below-minimum tests check every slippage minimum. If you write one invariant test for your own AMM, make it the round trip: swap in, swap back, and assert the pool did not lose value.
## What You Learnt from Building the Constant-Product AMM
-The order book needed a tree, a matching engine, and maker accounts, because humans were quoting the prices. The AMM replaced all of it, and the market makers themselves, with one multiplication: instant liquidity for anything, priced by a curve, funded by anyone willing to earn fees for supplying it.
+The order book needed a tree, a matching engine, and maker accounts, because humans were quoting the prices. The AMM replaced all of it, and the market makers themselves, with a pricing formula: instant liquidity for anything, priced by a curve, funded by anyone willing to earn fees for supplying it.
Six concepts came with the curve:
- **A formula as the market maker.** Output = taxed input × other reserve ÷ (this reserve + taxed input); the first deposit sets the price, and every trade pays its own price impact.
-- **How K changes.** Fixed while a trade is priced, growing with deposits and every swap's fee, shrinking with withdrawals; and an invariant that can hold vacuously (new K ≥ 0 on an empty pool) is not an invariant.
+- **How K changes.** Fixed while a trade is priced, growing with deposits and every swap's fee, shrinking with withdrawals; and the K check needs both reserves to be positive first, because on an empty pool new K ≥ 0 passes whatever the trade pays out.
- **Assert after the math.** The post-trade product re-check catches every "computed the output wrong and overpaid" bug, including the ones not written yet.
-- **Effective reserves.** The admin's virtual fee claim is excluded from every piece of LP-facing math, the order book's fee vault implemented as accounting.
+- **Effective reserves.** The admin's virtual fee claim is excluded from every piece of LP-facing math, and kept in two counters on the pool config rather than in a separate fee vault.
- **The inflation-attack defense.** A locked minimum of LP supply, never minted to anyone and counted in the divisor on the way in and on the way out, keeps the share divisor from ever being dust-sized.
-- **Impermanent loss.** An LP is a bet on trading volume, not price direction, and nobody should provide liquidity without being able to draw the diagram.
+- **Impermanent loss.** An LP is a bet on trading volume, not price direction: the further the price moves in either direction, the more the LP loses against simply holding the two tokens, and only fees make up for it.
-In the *Prop AMM* chapter we will build a market maker that quotes its own capital at prices taken from an oracle. There is no curve, so a trade does not move the price and there is no impermanent loss, but the firm carries a different risk: the traders who arrive while its quote is stale know something it does not. The *Managed Fund* chapter, later in the book, uses venues like these to trade other people's money, and the question there is what the manager is allowed to do with it.
+In the *Prop AMM* chapter we will build a market maker that quotes its own capital at prices taken from an oracle. There is no curve, so a trade does not move the price and there is no impermanent loss, but the firm carries a different risk: the traders who arrive while its quote is stale know something it does not. The *Managed Fund* chapter, later in the book, uses venues like these to trade investors' deposits, and the question there is what the manager is allowed to do with them.
diff --git a/building-financial-products-in-solana/escrow.md b/building-financial-products-in-solana/escrow.md
index 2d346de3..4df0bfb0 100644
--- a/building-financial-products-in-solana/escrow.md
+++ b/building-financial-products-in-solana/escrow.md
@@ -5,12 +5,12 @@ Escrow is the simplest program in this book. It lets two people swap tokens with
- holding tokens in a vault whose authority is an address only the program can sign for, so nobody, including the program's author, can move them except along the paths the code defines
- storing the terms of a deal in an account the program owns, and checking every later instruction against them
- letting who signed, and what the stored terms say, decide what may happen next
-- giving every state an exit that returns the tokens and the account rent deposits, so neither is stranded
+- giving every state a handler that empties the vault and closes the accounts, so no tokens or rent deposits are stranded
- counting every amount in minor units, the integers the token program moves
The chapters that follow add what escrow does not have: a formula that sets a price, an outcome that decides a payout, time and interest, and an operator who must be constrained.
-The program for this chapter lives at `finance/escrow` in the companion repository, with Anchor, Quasar, and native versions, plus machine-checked proofs of its core invariants under `kani-proofs`. This chapter covers the Anchor version.
+The program for this chapter lives at `finance/escrow` in the companion repository, with Anchor, Quasar, and native versions, plus Kani model checks of its core invariants under `kani-proofs`. This chapter covers the Anchor version.
## Escrow Lets Two Strangers Swap Without Trusting Each Other
@@ -24,7 +24,7 @@ Traditional finance solves this with a trusted third party. Alice sends her fund

-A Solana program replaces the third party. The program holds Alice's tokens under rules that are the deployed program itself: nobody, including the program's author, can move them except along the paths the code defines. There is no operator in this program at all: whoever deploys it earns nothing from it, takes no fee, and holds no key that matters. Each side receives one hundred percent of what the other side offered. Solana's transaction fees still apply, but they are a small fraction of a cent regardless of the size of the trade.
+A Solana program replaces the third party. The program holds Alice's tokens under rules that are the deployed program itself: nobody, including the program's author, can move them except along the paths the code defines. There is no operator in this program at all: whoever deploys it earns nothing from it, takes no fee, and holds no private key that matters. Each side receives one hundred percent of what the other side offered. Solana's transaction fees still apply, but they are a small fraction of a cent regardless of the size of the trade.

@@ -35,11 +35,11 @@ Two roles run through this chapter, and the code names them the same way:
- **Alice is the maker.** She creates the offer: 250 USDC for 1 TSLAx.
- **Bob is the taker.** He accepts the offer as it stands.
-The deal is priced the way all Solana money is priced: in **minor units**, the smallest denomination of each token, the raw integers programs operate on. USDC has six decimals, so Alice's 250 USDC is 250,000,000 minor units. TSLAx has eight decimals, so the single Tesla share she wants is 100,000,000 minor units, the same integer that means one hundred dollars in a USDC account. The same integer meaning two different things is why programs track each token's decimals instead of assuming them. The minor-units section of the *Arithmetic of Money* chapter covers this in depth.
+The deal is priced the way every Solana token amount is priced: in **minor units**, the smallest denomination of each token, the raw integers programs operate on. USDC has six decimals, so Alice's 250 USDC is 250,000,000 minor units. TSLAx has eight decimals, so the single Tesla share she wants is 100,000,000 minor units, the same integer that means one hundred dollars in a USDC account. The same integer meaning two different things is why programs track each token's decimals instead of assuming them. The minor-units section of the *Arithmetic of Money* chapter covers this in depth.
## The Instruction Handlers
-The program has three instruction handlers, and together they form the offer's complete state machine: `make_offer()` creates the offer and specifies what is wanted in return, and the offer stays open until someone else runs `take_offer()` or the maker runs `cancel_offer()`. Every state needs an exit, and an open offer has two.
+The program has three instruction handlers, and together they form the offer's complete state machine: `make_offer()` creates the offer and specifies what is wanted in return, and the offer stays open until someone else runs `take_offer()` or the maker runs `cancel_offer()`. Every participant must be able to get their assets out of every state, and an open offer can be ended by either of those two handlers.

@@ -76,7 +76,7 @@ With the structure verified, the handler code is two small steps:
### The Taker Takes the Offer
-Bob calls `take_offer()`. It takes no arguments beyond the accounts themselves: the offer Bob wishes to take is specified by the offer account he provides. The account constraints then verify that every other account in the transaction is the one that offer implies. The maker account must match the address stored in the offer, and the two mint accounts must match the mints stored there. The seeds and stored bump prove the offer account sits at the program's own PDA. The vault must be the offer's associated token account for mint A, and the payment destination must be the maker's associated token account for mint B. Bob cannot substitute an account he controls anywhere in this structure, because every address is derived or checked against the offer's stored data. This is the account-substitution defense the validation section of the *Adversary* chapter covers in full.
+Bob calls `take_offer()`. It takes no arguments beyond the accounts themselves: the offer Bob wishes to take is specified by the offer account he provides. The account constraints then verify that every other account in the transaction is the one that offer implies. The maker account must match the address stored in the offer, and the two mint accounts must match the mints stored there. The seeds and stored bump check that the offer account sits at the program's own PDA. The vault must be the offer's associated token account for mint A, and the payment destination must be the maker's associated token account for mint B. Bob cannot substitute an account he controls anywhere in this structure, because every address is derived or checked against the offer's stored data. This is the account-substitution defense the validation section of the *Adversary* chapter covers in full.

@@ -93,24 +93,24 @@ Both sides now hold exactly what the other offered. No percentage went to anyone
### The Maker Cancels the Offer
-If nobody takes the offer, Alice needs her USDC back. Without an exit, her tokens would sit in the vault forever, which is a stranded-funds bug. The exit is `cancel_offer()`.
+If nobody takes the offer, Alice needs her USDC back. Without a handler that returns them, her tokens would sit in the vault forever, which is a stranded-funds bug. That handler is `cancel_offer()`.

-The constraints make it maker-only: the signer must match the maker address stored in the offer. Bob cannot cancel Alice's offer, and the test suite proves it (more below). `cancel_offer()` mirrors the second half of `take_offer()`: it signs with the offer's seeds to move the full vault balance back to `maker_token_account_a`, and closes both the vault and the offer account, returning both rent deposits to Alice.
+The constraints make it maker-only: the signer must match the maker address stored in the offer. Bob cannot cancel Alice's offer, which `test_cancel_offer_rejects_non_maker` checks (more below). `cancel_offer()` mirrors the second half of `take_offer()`: it signs with the offer's seeds to move the full vault balance back to `maker_token_account_a`, and closes both the vault and the offer account, returning both rent deposits to Alice.
## The Math
-Escrow has less arithmetic than any other program in this book, because the offer is all-or-nothing: there are no partial fills, no division, and so no rounding policy to decide. The amounts the maker states are the amounts that move. The only multiplication in the program is in `transfer_checked()`, which the `transfer_tokens()` helper uses so that every transfer verifies the mint's decimals match what the caller believes they are, one more guard against minor-unit confusion.
+Escrow has less arithmetic than any other program in this book, because the offer is all-or-nothing: there are no partial fills, no division, and so no rounding policy to decide. The amounts the maker states are the amounts that move. The only multiplication in the program is in `transfer_checked()`, which the `transfer_tokens()` helper uses so that every transfer verifies the mint's decimals match what the caller believes they are, one more check against minor-unit confusion.
-The money math that remains is rent. Alice fronts the rent deposits for the offer account and the vault (and for her own token B account if it did not exist). On either exit path, both program-created accounts close and both deposits return to her; when her token B account already existed, her lamports round-trip exactly, and the tests assert that to the lamport. Bob pays rent only for his own token A account if he never held that token, and that account remains his afterward. Nobody's rent ends up with the other party.
+The lamport math that remains is rent. Alice fronts the rent deposits for the offer account and the vault (and for her own token B account if it did not exist). Whether the offer is taken or canceled, both program-created accounts close and both deposits return to her; when her token B account already existed, her lamports round-trip exactly, and the tests assert that to the lamport. Bob pays rent only for his own token A account if he never held that token, and that account remains his afterward. Nobody's rent ends up with the other party.
## The Invariants
Five things must hold before and after every handler:
- Alice's offered tokens are in exactly one place at any instant: her token account, the vault, or (after the take) Bob's token account.
-- The vault holds at least `token_a_offered_amount` from the end of `make_offer()` until an exit runs (a direct donation could push it above, and changes nothing), and both exits empty the vault completely before closing it.
+- The vault holds at least `token_a_offered_amount` from the end of `make_offer()` until `take_offer()` or `cancel_offer()` runs (a direct donation could push it above, and changes nothing), and both handlers empty the vault completely before closing it.
- The offer account's data never changes after `handle_save_offer()` writes it. There is no update handler, so there is nothing to get wrong.
- Only the taker's signature moves token B, and only the offer PDA's signature moves the vault.
- Rent returns to whoever paid it, on every path.
@@ -125,7 +125,7 @@ Five things must hold before and after every handler:
Each is closed by the constraint structure: associated token account derivation binds each token account to its owner and mint, stored-address checks bind the maker and mints to the offer's stored data, and seeds plus bump bind the offer to the program. The *Adversary* chapter shows what happens to programs that skip these checks.
- **Unauthorized cancel.** `cancel_offer()` requires the maker's signature and checks it against the offer's stored maker.
-- **The abandoned offer.** The failure mode that is a missing feature: without `cancel_offer()`, an untaken offer strands the maker's tokens permanently. Every state needs an exit.
+- **The abandoned offer.** If `cancel_offer()` didn't exist, an untaken offer would leave the maker's offered tokens stranded in the program.
- **Degenerate offers.** An offer with zero tokens on one side, or of a token for a different amount of itself, means someone is about to be cheated. `make_offer()` refuses an offer of zero tokens on either side with `ZeroAmount` (`test_make_offer_rejects_zero_offered_amount` and `test_make_offer_rejects_zero_wanted_amount`). An offer of a token for itself never reaches the handler: with one mint, the maker's token A and token B accounts are the same account, and Anchor refuses the same writable account twice with `ConstraintDuplicateMutableAccount`, which `test_make_offer_rejects_same_mint` checks.
## Testing Considerations
@@ -135,22 +135,22 @@ The tests live in `tests/test_escrow.rs`, creating the wallets, mints, and token
- `test_make_offer` calls `make_offer()` and then checks both effects: the vault holds the offered amount, and the offer account's stored fields (ID, maker) match what Alice sent.
- `test_take_offer` runs the full happy path and asserts both directions of the swap: Bob's token A balance and Alice's token B balance. Symmetric flows get tested in both directions, because a bug that favors one side is invisible if you only assert the other. It then asserts the vault and offer accounts are gone, and that Alice's lamports round-tripped exactly, and so asserts the rent economics.
- `test_cancel_offer` asserts Alice's token A balance returns to its pre-offer value and her rent comes back in full.
-- `test_cancel_offer_rejects_non_maker` is the adversarial case: Bob attempts to cancel Alice's offer and the transaction fails. Test the code path the bug lives in, and start with the one where money goes to the wrong person.
+- `test_cancel_offer_rejects_non_maker` is the adversarial case: Bob attempts to cancel Alice's offer and the transaction fails. Test the code path the bug lives in, and start with the one where tokens go to the wrong person.
-The testing section of the *Operating Financial Software* chapter builds the general method; this suite is the method at its smallest useful size. And escrow goes one step further: the `kani-proofs` crate in the companion repository proves the conservation invariants formally, over every possible input, with harnesses like `proof_take_offer_conserves_value` and `proof_cancel_offer_returns_all_to_maker`. The proving section of the *Operating Financial Software* chapter explains the technique.
+The testing section of the *Operating Financial Software* chapter builds the general method; this suite is the method at its smallest useful size. And escrow goes one step further: the `kani-proofs` crate in the companion repository model-checks the conservation invariants over every possible value, with harnesses like `proof_take_offer_conserves_value` and `proof_cancel_offer_returns_all_to_maker`. The model-checking section of the *Operating Financial Software* chapter explains the technique.
## What You Learnt from Building the Escrow
-In traditional finance, an escrow means a trusted third party: a company that holds both sides' assets, takes a percentage for the privilege, and must itself be trusted not to walk away. This chapter replaced that company with a program whose rules are its own deployed code, whose operator earns nothing and holds no key that matters, and whose fee is a fraction of a cent.
+In traditional finance, an escrow means a trusted third party: a company that holds both sides' assets, takes a percentage for the privilege, and must itself be trusted not to walk away. This chapter replaced that company with a program whose rules are its own deployed code, whose operator earns nothing and holds no private key that matters, and whose fee is a fraction of a cent.
The escrow gave you five concepts:
- **Program custody.** Tokens held in a vault whose authority is a PDA, an address only the program can sign for, so funds move only along the code paths the program defines.
- **Terms in a data account.** The offer account records the deal (maker, mints, wanted amount), and every later handler checks its accounts against that stored data.
-- **State transitions gated by signatures and stored terms.** Who signed, and what the stored data says, decide what may happen next; stored-address checks, seeds, and ATA derivation close every substitution.
-- **An exit from every state.** `take_offer()` and `cancel_offer()` both empty and close everything, returning the tokens and the account rent deposits to whoever they belong to, so neither is stranded.
+- **State transitions checked against signatures and stored terms.** Who signed, and what the stored data says, decide what may happen next; stored-address checks, seeds, and ATA derivation close every substitution.
+- **Every state has a handler that empties the vault.** `take_offer()` and `cancel_offer()` both empty and close everything, returning the tokens and the account rent deposits to whoever they belong to, so neither is stranded.
- **Minor units.** Every amount is an integer at the mint's declared scale, and identical integers can mean entirely different things.
-In the *Fundraiser* chapter we will build an escrow with many depositors instead of one maker. Contributions collect in one vault until a deadline, and the deadline decides which exit opens: the money goes to the person raising it if the target was reached, and back to every contributor if it was not. Most programs in the rest of this part keep this chapter's shape, a vault owned by a data account with an exit from every state, and change what decides the exit.
+In the *Fundraiser* chapter we will build an escrow with many depositors instead of one maker. Contributions collect in one vault until a deadline, and the deadline decides which handlers work: the contributions go to the person raising it if the target was reached, and back to every contributor if it was not. Most programs in the rest of this part keep this chapter's shape, a vault owned by a data account and, in every state, a handler that empties it, and change what decides who is paid.
[^remittance-cost]: The World Bank's figures for the third quarter of 2025, which put the global average near 6.5% with thirteen corridors above 20%. See [remittanceprices.worldbank.org](https://remittanceprices.worldbank.org).
diff --git a/building-financial-products-in-solana/figures/betting-alice-claim.svg b/building-financial-products-in-solana/figures/betting-alice-claim.svg
index 02fe7a04..dcc73f67 100644
--- a/building-financial-products-in-solana/figures/betting-alice-claim.svg
+++ b/building-financial-products-in-solana/figures/betting-alice-claim.svg
@@ -210,6 +210,6 @@
effects before interactions
3. bet account closed
300 stake + floor(300 × 196 ÷ 400) = 300 + 147 = 447; the 149 left is Bob's exact claim
- closing the bet account is the double-claim guard: no account, no second claim
+ closing the bet account prevents a double claim: no account, no second claim
diff --git a/building-financial-products-in-solana/figures/betting-settlement.svg b/building-financial-products-in-solana/figures/betting-settlement.svg
index ecc5272c..6378ca04 100644
--- a/building-financial-products-in-solana/figures/betting-settlement.svg
+++ b/building-financial-products-in-solana/figures/betting-settlement.svg
@@ -1,6 +1,6 @@
The vault splits at settlement
-
+
Solana Devnet
diff --git a/building-financial-products-in-solana/figures/escrow-program-swap.svg b/building-financial-products-in-solana/figures/escrow-program-swap.svg
index d38faef5..847018ab 100644
--- a/building-financial-products-in-solana/figures/escrow-program-swap.svg
+++ b/building-financial-products-in-solana/figures/escrow-program-swap.svg
@@ -13,7 +13,7 @@
Bob (taker)
1 SOL
-
+
escrow program
diff --git a/building-financial-products-in-solana/figures/escrow-state-machine.svg b/building-financial-products-in-solana/figures/escrow-state-machine.svg
index ff81f709..dadf41c7 100644
--- a/building-financial-products-in-solana/figures/escrow-state-machine.svg
+++ b/building-financial-products-in-solana/figures/escrow-state-machine.svg
@@ -1,5 +1,5 @@
- The offer's two exits
+ How an offer ends
diff --git a/building-financial-products-in-solana/figures/fundraiser-account-map.svg b/building-financial-products-in-solana/figures/fundraiser-account-map.svg
index 57020f43..82f4f283 100644
--- a/building-financial-products-in-solana/figures/fundraiser-account-map.svg
+++ b/building-financial-products-in-solana/figures/fundraiser-account-map.svg
@@ -157,7 +157,7 @@
time_started: day 0
duration: 7 days
-
+
THE FUNDRAISER'S ADDRESS +
diff --git a/building-financial-products-in-solana/figures/fundraiser-contribute.svg b/building-financial-products-in-solana/figures/fundraiser-contribute.svg
index 97cba248..4befca68 100644
--- a/building-financial-products-in-solana/figures/fundraiser-contribute.svg
+++ b/building-financial-products-in-solana/figures/fundraiser-contribute.svg
@@ -127,7 +127,7 @@
time_started: day 0
duration: 7 days
-
+
THE FUNDRAISER'S ADDRESS +
diff --git a/building-financial-products-in-solana/figures/fundraiser-initialize.svg b/building-financial-products-in-solana/figures/fundraiser-initialize.svg
index 60abcb59..bc9931de 100644
--- a/building-financial-products-in-solana/figures/fundraiser-initialize.svg
+++ b/building-financial-products-in-solana/figures/fundraiser-initialize.svg
@@ -115,7 +115,7 @@
duration: 7 days
NEW
-
+
THE FUNDRAISER'S ADDRESS +
diff --git a/building-financial-products-in-solana/figures/fundraiser-refunds.svg b/building-financial-products-in-solana/figures/fundraiser-refunds.svg
index c282e092..78c9f107 100644
--- a/building-financial-products-in-solana/figures/fundraiser-refunds.svg
+++ b/building-financial-products-in-solana/figures/fundraiser-refunds.svg
@@ -161,7 +161,7 @@
time_started: day 0
duration: 7 days
-
+
THE FUNDRAISER'S ADDRESS +
@@ -174,7 +174,7 @@
900 USDC
authority: the fundraiser PDA
-
+
900 USDC
diff --git a/building-financial-products-in-solana/figures/fundraiser-success.svg b/building-financial-products-in-solana/figures/fundraiser-success.svg
index a7b960b6..d5c41c31 100644
--- a/building-financial-products-in-solana/figures/fundraiser-success.svg
+++ b/building-financial-products-in-solana/figures/fundraiser-success.svg
@@ -145,7 +145,7 @@
-
+
THE FUNDRAISER'S ADDRESS +
diff --git a/building-financial-products-in-solana/figures/lending-borrow-ltv.svg b/building-financial-products-in-solana/figures/lending-borrow-ltv.svg
index d78e0f10..102ea999 100644
--- a/building-financial-products-in-solana/figures/lending-borrow-ltv.svg
+++ b/building-financial-products-in-solana/figures/lending-borrow-ltv.svg
@@ -185,7 +185,7 @@
borrow_obligation_liquidity()
repay_obligation_liquidity()
liquidate_obligation()
- collect_protocol_fees()
+ collect_program_fees()
@@ -206,7 +206,7 @@
available_liquidity: 250
borrowed_principal: 750
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -230,7 +230,7 @@
available_liquidity: 5
borrowed_principal: 0
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
diff --git a/building-financial-products-in-solana/figures/lending-deposit-shares.svg b/building-financial-products-in-solana/figures/lending-deposit-shares.svg
index fcf13403..1ef504d0 100644
--- a/building-financial-products-in-solana/figures/lending-deposit-shares.svg
+++ b/building-financial-products-in-solana/figures/lending-deposit-shares.svg
@@ -132,7 +132,7 @@
borrow_obligation_liquidity()
repay_obligation_liquidity()
liquidate_obligation()
- collect_protocol_fees()
+ collect_program_fees()
@@ -153,7 +153,7 @@
available_liquidity: 1,000
borrowed_principal: 0
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
diff --git a/building-financial-products-in-solana/figures/lending-liquidation-second-call.svg b/building-financial-products-in-solana/figures/lending-liquidation-second-call.svg
index 217a0be0..697321d2 100644
--- a/building-financial-products-in-solana/figures/lending-liquidation-second-call.svg
+++ b/building-financial-products-in-solana/figures/lending-liquidation-second-call.svg
@@ -227,7 +227,7 @@
borrow_obligation_liquidity()
repay_obligation_liquidity()
liquidate_obligation()
- collect_protocol_fees()
+ collect_program_fees()
@@ -248,7 +248,7 @@
available_liquidity: 1,250
borrowed_principal: 750
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -285,7 +285,7 @@
available_liquidity: 3.25
borrowed_principal: 0.75
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -309,7 +309,7 @@
4
1
- Health check gates the call
+ Carol must be unhealthy
2
Repay 0.75 SPCXx into the reserve
3
diff --git a/building-financial-products-in-solana/figures/lending-liquidation.svg b/building-financial-products-in-solana/figures/lending-liquidation.svg
index b2b5d631..40818ec7 100644
--- a/building-financial-products-in-solana/figures/lending-liquidation.svg
+++ b/building-financial-products-in-solana/figures/lending-liquidation.svg
@@ -227,7 +227,7 @@
borrow_obligation_liquidity()
repay_obligation_liquidity()
liquidate_obligation()
- collect_protocol_fees()
+ collect_program_fees()
@@ -248,7 +248,7 @@
available_liquidity: 1,250
borrowed_principal: 750
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -285,7 +285,7 @@
available_liquidity: 2.5
borrowed_principal: 1.5
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -309,7 +309,7 @@
4
1
- Health check gates the call
+ Carol must be unhealthy
2
Repay 1.5 SPCXx into the reserve
3
diff --git a/building-financial-products-in-solana/figures/lending-open-market.svg b/building-financial-products-in-solana/figures/lending-open-market.svg
index 6ccae2d2..faddc56f 100644
--- a/building-financial-products-in-solana/figures/lending-open-market.svg
+++ b/building-financial-products-in-solana/figures/lending-open-market.svg
@@ -118,7 +118,7 @@
borrow_obligation_liquidity()
repay_obligation_liquidity()
liquidate_obligation()
- collect_protocol_fees()
+ collect_program_fees()
@@ -145,7 +145,7 @@
NEW
borrowed_principal: 0
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -194,7 +194,7 @@
NEW
borrowed_principal: 0
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -243,7 +243,7 @@
NEW
borrowed_principal: 0
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
diff --git a/building-financial-products-in-solana/figures/lending-open-reserves.svg b/building-financial-products-in-solana/figures/lending-open-reserves.svg
index 9ebbe245..33791d21 100644
--- a/building-financial-products-in-solana/figures/lending-open-reserves.svg
+++ b/building-financial-products-in-solana/figures/lending-open-reserves.svg
@@ -82,7 +82,7 @@
borrow_obligation_liquidity()
repay_obligation_liquidity()
liquidate_obligation()
- collect_protocol_fees()
+ collect_program_fees()
@@ -117,7 +117,7 @@
available_liquidity: 1,001 minor units
borrowed_principal: 0
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -173,7 +173,7 @@
available_liquidity: 1,001 minor units
borrowed_principal: 0
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
@@ -229,7 +229,7 @@
available_liquidity: 1,001 minor units
borrowed_principal: 0
borrow_accumulation_factor: 1.0
- accumulated_protocol_fees: 0
+ accumulated_program_fees: 0
diff --git a/building-financial-products-in-solana/figures/options-state-machine.svg b/building-financial-products-in-solana/figures/options-state-machine.svg
index 0a31bcaa..dafede94 100644
--- a/building-financial-products-in-solana/figures/options-state-machine.svg
+++ b/building-financial-products-in-solana/figures/options-state-machine.svg
@@ -1,5 +1,5 @@
- The option's three states and three exits
+ Three states and three closing handlers
@@ -26,7 +26,7 @@
option closed
rent refunded to the writer
-
+
cancel_option()
(collateral back)
@@ -37,6 +37,6 @@
collect_proceeds()
(the strike, or the delivery)
- expiry is not a state: it is the clock deciding which of two exits a held option has,
- and at every instant exactly one of them is open
+ expiry is not a state: before it, only exercise_option() works on a held option,
+ and at or after it, only reclaim_collateral() does
diff --git a/building-financial-products-in-solana/figures/order-book-alice-ask.svg b/building-financial-products-in-solana/figures/order-book-alice-ask.svg
index 18c1b9f1..a2c61f9b 100644
--- a/building-financial-products-in-solana/figures/order-book-alice-ask.svg
+++ b/building-financial-products-in-solana/figures/order-book-alice-ask.svg
@@ -215,7 +215,7 @@
fee vault
-
+
10 NVDAx
locked on entry by place_order(): the order is funded before it rests
diff --git a/building-financial-products-in-solana/figures/order-book-cancel.svg b/building-financial-products-in-solana/figures/order-book-cancel.svg
index 3d4bd679..134da6ae 100644
--- a/building-financial-products-in-solana/figures/order-book-cancel.svg
+++ b/building-financial-products-in-solana/figures/order-book-cancel.svg
@@ -1,7 +1,7 @@
- Bob cancels, and the lock returns
-
-
+ Bob cancels, and his USDC is credited back
+
+
Solana Devnet
diff --git a/building-financial-products-in-solana/figures/order-book-settle.svg b/building-financial-products-in-solana/figures/order-book-settle.svg
index ad9cdac3..c0a2e504 100644
--- a/building-financial-products-in-solana/figures/order-book-settle.svg
+++ b/building-financial-products-in-solana/figures/order-book-settle.svg
@@ -237,7 +237,7 @@
-
+
each handler zeroes the counters before it makes the transfers:
checks-effects-interactions, so a re-entrant caller finds nothing left
diff --git a/building-financial-products-in-solana/figures/order-book-state-machine.svg b/building-financial-products-in-solana/figures/order-book-state-machine.svg
index 4bc06d74..2929f464 100644
--- a/building-financial-products-in-solana/figures/order-book-state-machine.svg
+++ b/building-financial-products-in-solana/figures/order-book-state-machine.svg
@@ -18,7 +18,7 @@
one taker crosses the whole order
- cancel_order() or eviction, remaining lock credited back
+ cancel_order() or eviction, held tokens credited back
cancel_order() or eviction
diff --git a/building-financial-products-in-solana/figures/order-book-withdraw-fees.svg b/building-financial-products-in-solana/figures/order-book-withdraw-fees.svg
index 9509b8c2..3c48f5fc 100644
--- a/building-financial-products-in-solana/figures/order-book-withdraw-fees.svg
+++ b/building-financial-products-in-solana/figures/order-book-withdraw-fees.svg
@@ -1,7 +1,7 @@
Maria withdraws the fees
-
-
+
+
Solana Devnet
diff --git a/building-financial-products-in-solana/figures/perps-account-map.svg b/building-financial-products-in-solana/figures/perps-account-map.svg
index 2a527c07..353b850e 100644
--- a/building-financial-products-in-solana/figures/perps-account-map.svg
+++ b/building-financial-products-in-solana/figures/perps-account-map.svg
@@ -173,7 +173,7 @@
liquidity: 12,000 USDC
reserved_liquidity: 10,000
total_collateral: 1,990
- protocol_fees: 10
+ program_fees: 10
long_size: 5,000
short_size: 5,000
cumulative_funding: 0
diff --git a/building-financial-products-in-solana/figures/perps-create-pool.svg b/building-financial-products-in-solana/figures/perps-create-pool.svg
index be0fa429..19289a13 100644
--- a/building-financial-products-in-solana/figures/perps-create-pool.svg
+++ b/building-financial-products-in-solana/figures/perps-create-pool.svg
@@ -121,7 +121,7 @@
NEW
reserved_liquidity: 0
total_collateral: 0
- protocol_fees: 0
+ program_fees: 0
long_size: 0
short_size: 0
cumulative_funding: 0
diff --git a/building-financial-products-in-solana/figures/perps-liquidation.svg b/building-financial-products-in-solana/figures/perps-liquidation.svg
index d50e2d0c..f303cc2d 100644
--- a/building-financial-products-in-solana/figures/perps-liquidation.svg
+++ b/building-financial-products-in-solana/figures/perps-liquidation.svg
@@ -155,7 +155,7 @@
liquidity: 12,750 USDC
reserved_liquidity: 5,000
total_collateral: 995
- protocol_fees: 10
+ program_fees: 10
long_size: 5,000
short_size: 0
cumulative_funding: 0
diff --git a/building-financial-products-in-solana/fundraiser.md b/building-financial-products-in-solana/fundraiser.md
index 454598d8..06728fd1 100644
--- a/building-financial-products-in-solana/fundraiser.md
+++ b/building-financial-products-in-solana/fundraiser.md
@@ -1,6 +1,6 @@
# Chapter 12: The Fundraiser
-Maria has designed a compact commercial espresso machine, and the factory quoted 9,000 USDC for the first production run. The cost is indivisible: the injection-mold tooling has to be cut whole, so 6,000 USDC buys not two-thirds of a run but nothing at all. She could collect preorder promises by hand and hope enough of them are real, or she could put one rule onchain: **everyone's money comes back unless the whole amount shows up**. This chapter's program enforces that rule. It is an all-or-nothing fundraiser, the same promise Kickstarter makes, enforced by code instead of by a company.
+Maria has designed a compact commercial espresso machine, and the factory quoted 9,000 USDC for the first production run. The cost is indivisible: the injection-mold tooling has to be cut whole, so 6,000 USDC buys not two-thirds of a run but nothing at all. She could collect preorder promises by hand and hope enough of them are real, or she could put one rule onchain: **everyone's contribution comes back unless the full target amount is raised**. This chapter's program enforces that rule. It is an all-or-nothing fundraiser, the same promise Kickstarter makes, enforced by code instead of by a company.
The program lives at `finance/fundraiser` in the companion repository. It is one of the smallest programs in this part of the book: six instruction handlers, two account types, and one vault. Read it as the *Escrow* chapter with the parties rearranged. An escrow locks two strangers' assets until both sides of one deal are ready; a fundraiser locks many strangers' contributions until one shared condition is met. The vault and the PDA that signs for it work the same way, with one addition: a **deadline**.
@@ -8,7 +8,7 @@ The program calls the fundraiser's creator the **maker**, Maria here. She wants
## Success and Failure Are Both Written Down in Advance
-Two numbers are recorded the moment the fundraiser opens: the **target** and the **duration**. Together they define exactly two endings. If contributions reach the target, Maria takes everything. If the deadline passes first, every contributor takes back exactly what they put in. There is no third outcome, no partial award, and no moment where anyone has to trust Maria with money she has not yet earned: until the target is hit, her claim handler refuses to run.
+Two numbers are recorded the moment the fundraiser opens: the **target** and the **duration**. Together they define exactly two endings. If contributions reach the target, Maria takes everything. If the deadline passes first, every contributor takes back exactly what they put in. There is no third outcome, no partial award, and no moment where anyone has to trust Maria with USDC she has not yet earned: until the target is hit, her claim handler refuses to run.
A fundraiser can only end one way. The claim requires the total to have reached the target and a refund requires it not to have, so whichever one the total allows, it rules out the other.
@@ -20,7 +20,7 @@ Maria calls `initialize_fundraiser()` with a target of 9,000 USDC and a duration
This transaction created the two accounts the raise needs.
-The **fundraiser** account lives at a PDA with seeds `"fundraiser"` and the maker's address, which means one live fundraiser per maker; a second `initialize_fundraiser()` from the same key finds the account already in existence and fails. Its struct, `Fundraiser`, holds the raise's terms:
+The **fundraiser** account lives at a PDA with seeds `"fundraiser"` and the maker's address, which means one live fundraiser per maker; a second `initialize_fundraiser()` from the same public key finds the account already in existence and fails. Its struct, `Fundraiser`, holds the raise's terms:
- `maker` and `mint_to_raise`: who is raising, and in what token, USDC here.[^any-mint]
- `amount_to_raise`: the target, 9,000 USDC in minor units. `initialize_fundraiser()` rejects a target below three major units with `InvalidAmount`, so a dust-sized raise cannot exist.
@@ -39,7 +39,7 @@ Maria wants the raise to show that many people want the machine, so one large ba
Alice tries to contribute her entire 1,000 USDC. The program refuses with `ContributionTooBig`. Her limit, computed by `calculate_max_contribution()`, is 10% of the target: 900 USDC. The constants behind it, `MAX_CONTRIBUTION_PERCENTAGE` over `PERCENTAGE_SCALER`, read 10/100, and the multiply runs in u128 before one floor division, multiply before divide, as in every other chapter.
-She contributes 900 USDC instead, and every backer makes the same call: `contribute()` moves the tokens from her own token account into the vault, and adds 900 to `current_amount`. The program can deduct from Alice's token account because she signed the transaction.
+She contributes 900 USDC instead, and every backer makes the same call: `contribute()` moves the tokens from Alice's own token account into the vault, and adds 900 to `current_amount`. The program can deduct from Alice's token account because she signed the transaction.

@@ -47,7 +47,7 @@ The transaction also created an account for her. Each backer gets a **contributo
The cap applies to each backer's total across all their calls. Carol contributes 450 USDC, then comes back the next day with 450 more, and her contributor account's running total reaches the 900 cap. A third contribution would fail with `MaximumContributionsReached`. And at the bottom end, anything under one major unit rejects with `ContributionTooSmall`, so nobody spams the raise with dust.
-So the money arrives like this: Alice 900, Bob 900, Carol 450 and 450, and eight more backers between 550 and 900 apiece. Ten backers is the minimum; this raise took eleven because most of them gave less than the cap. Total: exactly 9,000 USDC.
+So the contributions arrive like this: Alice 900, Bob 900, Carol 450 and 450, and eight more backers between 550 and 900 apiece. Ten backers is the minimum; this raise took eleven because most of them gave less than the cap. Total: exactly 9,000 USDC.

@@ -55,7 +55,7 @@ So the money arrives like this: Alice 900, Bob 900, Carol 450 and 450, and eight
The claim handler compares the target against `current_amount`, the program's own count of accepted contributions, and **never against the vault's token balance**. Anyone can transfer tokens to any token account on Solana, no permission required. If the check read the vault balance, Maria could top her own vault up with 5,400 USDC on day six, "reach" the target, and walk off with 3,600 USDC of contributions the target was meant to protect. Because the check reads the counter, that transfer changes nothing: the test `test_check_contributions_ignores_direct_vault_donations` mints the full target straight into the vault and asserts that the claim still fails.
-The same applies to any check that releases money: **it should read a number the program itself wrote**, because anyone can pass a check on a token balance by transferring tokens in.
+The same applies to any check that releases tokens from the vault: **it should read a number the program itself wrote**, because anyone can pass a check on a token balance by transferring tokens in.
## The Target Is Met: The Maker Claims, and Everything Closes
@@ -94,7 +94,7 @@ Every addition and subtraction on `current_amount` and the receipts is checked a
- Tokens that entered through `contribute()` equal `current_amount` plus tokens already refunded; the vault can only exceed that by direct donations, which no check ever reads.
- No contributor's receipt ever exceeds 10% of the target.
- The claim requires `current_amount` to be at least the target and the refund requires it to be below, so a fundraiser can only end one way.
-- Money leaves the vault only through the claim or a refund, both signed by the fundraiser PDA, the claim requiring the maker's signature and the counter at the target, a refund requiring the contributor's signature, a passed deadline, and their own receipt.
+- Tokens leave the vault only through the claim or a refund, both signed by the fundraiser PDA, the claim requiring the maker's signature and the counter at the target, a refund requiring the contributor's signature, a passed deadline, and their own receipt.
## The Failure Modes: One Check per Attack
@@ -102,30 +102,36 @@ Every addition and subtraction on `current_amount` and the receipts is checked a
- **Refunding early or after a win** fails on the deadline or the counter: `FundraiserNotEnded`, `TargetMet` (checked by `test_refund_before_deadline_fails` and `test_refund_when_target_met_fails`).
- **Contributing after the deadline** fails with `FundraiserEnded` (checked by `test_contribute_after_deadline_fails`).
- **Capture by one large backer** fails on the cumulative cap: `ContributionTooBig` per call, `MaximumContributionsReached` across calls (checked by `test_contribute_above_cap_fails` and `test_cumulative_contributions_above_cap_fail`).
-- **Inflating the vault directly** moves tokens but never moves the counter, so it unlocks nothing.
+- **Inflating the vault directly** moves tokens but never moves the counter, so the claim still fails with `TargetNotMet` (checked by `test_check_contributions_ignores_direct_vault_donations`).
- **Double refunds** fail because the receipt account is gone.
- **Dust contributions** fail on the one-major-unit minimum: `ContributionTooSmall`.
-- **Closing the fundraiser early or with money inside** fails with `FundraiserNotEnded` before the deadline, `TargetMet` after a win, and `RefundsOutstanding` while any contribution remains unrefunded.
+- **Closing the fundraiser early or with contributions inside** fails with `FundraiserNotEnded` before the deadline, `TargetMet` after a win, and `RefundsOutstanding` while any contribution remains unrefunded.
- **Closing a receipt while its raise is live** fails because the fundraiser account still belongs to the program: `FundraiserStillOpen` (checked by `test_close_contributor_while_fundraiser_open_fails`).
## Testing Considerations
-The suite warps the clock with a helper that advances whole days, and asserts balances after every move. Three tests follow a whole lifecycle: `test_check_contributions_success_pays_maker_and_closes_vault` funds the raise with ten contributors at the cap and claims it, `test_refund_after_deadline_target_not_met_succeeds` follows the losing timeline to a clean refund, and `test_close_fundraiser_after_failed_raise_allows_a_new_raise` closes the failed fundraiser and checks the same wallet can raise again. The rest each call a handler in a state where it must refuse, and assert the error, such as `test_refund_before_deadline_fails`. Every vault program needs a donation test: mint tokens straight into your vault outside your own handlers, then assert that nothing unlocks (`test_check_contributions_ignores_direct_vault_donations` on the claim; `test_close_fundraiser_sweeps_direct_donations_to_maker` checks that `close_fundraiser()` pays such tokens to the maker).
+The suite warps the clock with a helper that advances whole days, and asserts balances after every move. Three tests follow a whole lifecycle:
+
+- `test_check_contributions_success_pays_maker_and_closes_vault` funds the raise with ten contributors at the cap and claims it.
+- `test_refund_after_deadline_target_not_met_succeeds` follows the losing timeline to a clean refund.
+- `test_close_fundraiser_after_failed_raise_allows_a_new_raise` closes the failed fundraiser and checks the same wallet can raise again.
+
+The rest each call a handler in a state where it must refuse, and assert the error, such as `test_refund_before_deadline_fails`. Every vault program needs a donation test: mint tokens straight into your vault outside your own handlers, then assert that nothing unlocks (`test_check_contributions_ignores_direct_vault_donations` on the claim; `test_close_fundraiser_sweeps_direct_donations_to_maker` checks that `close_fundraiser()` pays such tokens to the maker).
## What You Learnt from Building the Fundraiser
-Kickstarter's all-or-nothing promise is a company policy: the platform holds the pledges, takes its cut, and can change campaign rules mid-campaign. This chapter's program makes the same promise in code: no platform, no fee, and terms that cannot change because no handler exists to change them.
+Kickstarter's all-or-nothing promise is a company policy: the platform holds the pledges, takes its cut, and can change campaign rules mid-campaign. This chapter's program enforces the same all-or-nothing outcome in code: no platform, no fee, and terms that cannot change because no handler exists to change them.
The fundraiser added five concepts on top of the escrow's:
- **A deadline that decides which handlers work.** One floored day-count expression, compared both ways, so contributions and refunds can never both be legal at once.
-- **Checks read the program's counter, not the vault balance.** A check that releases money must read a number the program itself wrote, never a token balance anyone can raise by transferring tokens in.
+- **Checks read the program's counter, not the vault balance.** A check that releases tokens from a vault must read a number the program itself wrote, never a token balance anyone can raise by transferring tokens in.
- **A receipt account per participant**, at a PDA, created on first use, added to on later contributions, and closed on refund, so a second refund finds nothing to read.
- **Cumulative caps.** Per-participant limits checked against running totals, so a raise cannot succeed without many participants.
-- **Name the closer for every account.** A successful raise closes the vault and the fundraiser but cannot reach the receipts, so a handler exists whose only job is to close one once the fundraiser is gone.
+- **Every account has a handler that closes it.** A successful raise closes the vault and the fundraiser but cannot reach the receipts, so a handler exists whose only job is to close one once the fundraiser is gone.
-In the *Betting Market* chapter we will build a pool that many people pay into, where who gets paid is decided by something the program cannot see: which outcome happened in the real world. Someone has to tell the program the answer, and you will see how the program limits what that person can do with the money while trusting them for the result.
+In the *Betting Market* chapter we will build a pool that many people pay into, where who gets paid is decided by something the program cannot see: which outcome happened in the real world. Someone has to tell the program the answer, and you will see how the program limits what that person can do with the pooled stakes while trusting them for the result.
[^any-mint]: Any mint works, Token Extensions mints included, because every account is declared through the token interface.
-[^proof-of-personhood]: The cap counts wallets, and a wallet costs nothing to create, so ten receipts prove ten wallets rather than ten people. A raise that needs distinct people has to bring that proof from outside the program. Proof-of-personhood services exist to supply it: a person proves once, to the service, that they are a unique human, and a program can then require that proof before it accepts a contribution. Two such services are World ID ([world.org](https://world.org)) and VeryAI ([very.org](https://very.org)). How each proof reaches a Solana program differs by service, and checking one is outside this book.
+[^proof-of-personhood]: The cap counts wallets, and a wallet costs nothing to create, so ten receipts show ten wallets rather than ten people. A raise that needs distinct people has to bring that proof from outside the program. Proof-of-personhood services exist to supply it: a person proves once, to the service, that they are a unique human, and a program can then require that proof before it accepts a contribution. Two such services are World ID ([world.org](https://world.org)) and VeryAI ([very.org](https://very.org)). How each proof reaches a Solana program differs by service, and checking one is outside this book.
diff --git a/building-financial-products-in-solana/lending.md b/building-financial-products-in-solana/lending.md
index e2aa4e69..0c0d5ede 100644
--- a/building-financial-products-in-solana/lending.md
+++ b/building-financial-products-in-solana/lending.md
@@ -1,12 +1,12 @@
# Chapter 17: Lending and Borrowing
-Start with a commercial bank. Savers deposit money and earn interest. Borrowers take loans and pay interest. The bank sits in the middle, collecting from the borrowers, paying most of it to the savers, and keeping a slice for itself. The onchain equivalent is a borrow-lend market, usually just called a **lending market**: Save, Kamino Lend, and MarginFi are popular lending markets on Solana, and lending is one of the largest categories of capital on the network.[^lending-size] The program for this chapter lives at `finance/lending`, and it is the largest program in the book so far: interest that compounds on every refresh, collateral that gets marked to market, and a liquidation engine that keeps the whole thing solvent when prices move.
+Start with a commercial bank. Savers make deposits and earn interest. Borrowers take loans and pay interest. The bank sits in the middle, collecting from the borrowers, paying most of it to the savers, and keeping a slice for itself. The onchain equivalent is a borrow-lend market, usually just called a **lending market**: Save, Kamino Lend, and MarginFi are popular lending markets on Solana, and lending is one of the largest categories of capital on the network.[^lending-size] The program for this chapter lives at `finance/lending`, and it is the largest program in the book so far: interest that compounds on every refresh, collateral that gets marked to market, and a liquidation engine that keeps the whole thing solvent when prices move.
One word is overloaded. This chapter's market trades stocks like NVDAx and SPCXx, which people colloquially call "shares." **In this chapter, a share is a share of a reserve**: a token minted by the program representing your claim on a pool of deposited assets, including the interest it earns. The stocks are just tokens.
## Prices Come In by Signature: The Feed, Freshness, and Slots
-Every valuation in this chapter multiplies an amount by a price, and the prices come from **price feed** accounts (seeds: `"price_feed"`, the market, the mint) storing a mantissa and exponent. The layout mirrors the account a price feed network publishes: a signed mantissa, an exponent, and the slot the value was written. In production the account would *be* the network's feed, decoded and verified by the program with the network's own crate, and a production read should also reject prices whose confidence interval is too wide. In this example, `set_price()` writes it directly (only the market owner may sign, and the test `non_owner_cannot_write_market_price_feed` checks that), which keeps the tests deterministic and makes the trust arrangement unusually visible: this market's prices are true because a named key signed them recently.
+Every valuation in this chapter multiplies an amount by a price, and the prices come from **price feed** accounts (seeds: `"price_feed"`, the market, the mint) storing a mantissa and exponent. The layout mirrors the account a price feed network publishes: a signed mantissa, an exponent, and the slot the value was written. In production the account would *be* the network's feed, decoded and verified by the program with the network's own crate, and a production read should also reject prices whose confidence interval is too wide. In this example, `set_price()` writes it directly (only the market owner may sign, and the test `non_owner_cannot_write_market_price_feed` checks that), which keeps the tests deterministic and makes the trust arrangement unusually visible: the program accepts a price because the market owner's private key signed it recently.
"Recently" is enforced, and it is enforced in **slots**: any price older than 25 slots makes the reading handler fail with `StalePriceFeed`, slots being the clock the *Time* chapter recommends whenever an adversary running a leader could profit by shifting the time a few seconds. Every handler that values anything requires `refresh_reserve()` and `refresh_obligation()` to have run earlier in the same transaction. A transaction that leaves out the reserve refresh fails with `ReserveStale`, and one that leaves out the obligation refresh fails with `ObligationStale`.
@@ -20,7 +20,7 @@ Financial state on Solana is accounts, and opening the market created thirteen o
The **lending market** account (PDA seeds: `"lending_market"`, a market ID) stores the `owner` (Maria, used only for authorization, never in the address) and the `quote_currency_mint`, the stable asset all values are measured in: USDC. Distinct market IDs give independent, risk-isolated pools.
-Each asset gets a **reserve** account (PDA seeds: `"reserve"`, the market, the asset's mint). The reserve's PDA is the authority of two accounts: a **liquidity vault** holding deposited tokens, and a **share mint** whose tokens are suppliers' claims on the vault plus its accrued interest. The reserve tracks four things: `available_liquidity`, `borrowed_principal`, the `borrow_accumulation_factor` that makes interest work (below), and `accumulated_protocol_fees`, Maria's slice, carved out so it never inflates the suppliers' exchange rate. `available_liquidity` is the un-borrowed pool, and it is deliberately the source of truth instead of the vault's raw balance, so donating tokens to the vault cannot move the exchange rate.
+Each asset gets a **reserve** account (PDA seeds: `"reserve"`, the market, the asset's mint). The reserve's PDA is the authority of two accounts: a **liquidity vault** holding deposited tokens, and a **share mint** whose tokens are suppliers' claims on the vault plus its accrued interest. The reserve tracks four things: `available_liquidity`, `borrowed_principal`, the `borrow_accumulation_factor` that makes interest work (below), and `accumulated_program_fees`, Maria's slice, carved out so it never inflates the suppliers' exchange rate. `available_liquidity` is the un-borrowed pool, and the program deliberately prices shares from it instead of from the vault's raw balance, so donating tokens to the vault cannot move the exchange rate.
Each reserve carries a `ReserveConfig`, all in basis points, and its validation rules are the credit-risk controls:
@@ -52,7 +52,7 @@ Bob needs cash but does not want to sell his five NVDAx (NVIDIA stock, $200 each
3. `deposit_obligation_collateral()`: moves his five NVDAx shares into a collateral vault owned by his obligation. What he pledges is *shares*, not raw tokens, so his collateral keeps earning supplier yield while it is pledged.
4. `borrow_obligation_liquidity()`: with his collateral worth $1,000 and loan-to-value at 75%, he may borrow up to $750 of USDC, which he does, drawn from the USDC reserve's available liquidity.
-The health check behind step 4 is conservative by construction: when `refresh_obligation()` values his position, collateral value is *floored* and debt value is *ceiled*, so every health evaluation errs against the borrower. Borrowing one unit past the allowance fails (`BorrowTooLarge`), and the test `borrow_up_to_max_ltv_then_one_more_fails` checks the boundary from both sides. His NVDAx is not sold, and he gets it back the moment he repays.
+The health check behind step 4 errs against the borrower: when `refresh_obligation()` values Bob's position, collateral value is *floored* and debt value is *ceiled*, so every health evaluation errs against the borrower. Borrowing one unit past the allowance fails (`BorrowTooLarge`), and the test `borrow_up_to_max_ltv_then_one_more_fails` checks the boundary from both sides. Bob's NVDAx is not sold, and he gets it back the moment he repays.
The second step created Bob's **obligation**, one account per borrower per market (PDA seeds: `"obligation"`, the market, the owner): his loan file, listing collateral deposits and borrows (up to four reserves each). It also caches values (`deposited_value`, `borrowed_value`, `allowed_borrow_value`, `unhealthy_borrow_value`) behind a `stale` flag that forces revaluation before anything financial happens.
@@ -76,7 +76,7 @@ The rate itself comes from a **kinked utilization curve**. Utilization is borrow
Those percentages are annual, and the accrual is per second, so the conversion divides by the seconds in a year. The seconds elapsed are the Clock's timestamp minus the one the reserve stored at its last accrual, `last_accrual_timestamp`. Counting seconds rather than slots keeps a borrower's cost to the quoted rate whatever the network's slot length, and the *Time* chapter covers why. Changing the curve with `update_reserve_config()` accrues at the old rates first, so the seconds already elapsed are charged at the rates that applied to them.
-Of the interest that accrues, the reserve factor (10%) is set aside in `accumulated_protocol_fees`, floored, in the suppliers' favor; the rest belongs to the suppliers through the rising share price.
+Of the interest that accrues, the reserve factor (10%) is set aside in `accumulated_program_fees`, floored, in the suppliers' favor; the rest belongs to the suppliers through the rising share price.

@@ -84,11 +84,11 @@ Of the interest that accrues, the reserve factor (10%) is set aside in `accumula
The other thing that moves over time is prices. SPCXx rallies from $250 to $300, and the feed says so. Carol's debt is now 3 × $300 = $900 against $1,000 of collateral, and her liquidation threshold is 80%: $800. Her `borrowed_value` exceeds her `unhealthy_borrow_value`, and the market will now pay anyone to fix that.
-## A Liquidator Closes the Gap, Twice, by the Rules
+## A Liquidator Closes the Gap, Twice
Dave watches for liquidation opportunities. He is the reason the market stays solvent. He runs `liquidate_obligation()`, repaying some of Carol's SPCXx debt in exchange for her USDC collateral at a 5% bonus. The handler enforces four rules:
-- A healthy obligation cannot be touched: the health gate rejects with `ObligationHealthy` (checked by `healthy_obligation_cannot_be_liquidated`). A liquidator cannot choose their target; they can only take positions the market has already marked unhealthy.
+- A healthy obligation cannot be touched: the handler rejects it with `ObligationHealthy` (checked by `healthy_obligation_cannot_be_liquidated`). A liquidator cannot choose their target; they can only take positions the market has already marked unhealthy.
- The **close factor** caps one liquidation at 50% of the debt. Dave repays 1.5 SPCXx ($450).
- The **bonus** prices his service: he seizes $450 × 1.05 = $472.50 of Carol's collateral, paid in USDC shares, every step of the conversion floored toward the borrower.
- Overreach fails the whole call: asking to seize more collateral than the obligation holds rejects with `LiquidationTooLarge` rather than silently capping, because a silent cap would make the liquidator pay full price for less collateral (checked by `over_seizing_liquidation_rejected_smaller_succeeds`).
@@ -103,9 +103,9 @@ Carol is still slightly unhealthy, so Dave goes again: half of the remaining 1.5
## Cranks: Work With No Natural Caller
-Dave is one instance of a pattern this market runs on. Some work in a financial product has no natural caller: interest must accrue, cached prices must refresh, and no user's own transaction is obliged to do any of it. The onchain pattern is the **crank**: an instruction handler that anyone may call, safe to call at any time, so that keeping the market's state current is a permissionless job rather than an operator's promise. A good crank has three properties:
+Dave is one instance of a pattern this market runs on. Some work in a financial product has no natural caller: interest must accrue, cached prices must refresh, and no user's own transaction is obliged to do any of it. The onchain pattern is the **crank**: an instruction handler that anyone may call, safe to call at any time, so that keeping the market's state current is a permissionless job rather than one only the operator can do. A good crank has three properties:
-- **Permissionless: the handler checks state, not identity.** If the work is valid, anyone's signature will do. `refresh_reserve()` and `refresh_obligation()` are the canonical examples: they bring interest and cached prices up to date for whoever asks, no special key required, and gating maintenance on the operator's key would turn the operator's absence into everyone's outage. Anyone can run the crank, so a crank that moves funds must calculate their destination from information the program itself stores in its own accounts, never by paying the signer: a crank that pays whoever called has made its funds claimable by strangers.
+- **Permissionless: the handler checks state, not identity.** If the work is valid, anyone's signature will do. `refresh_reserve()` and `refresh_obligation()` are the canonical examples: they bring interest and cached prices up to date for whoever asks, no special signer required, and requiring the operator's signature for maintenance would turn the operator's absence into everyone's outage. Anyone can run the crank, so a crank that moves funds must calculate their destination from information the program itself stores in its own accounts, never by paying the signer: a crank that pays whoever called has made its funds claimable by strangers.
- **Idempotent: a no-op succeeds.** Calling the crank when there is nothing to do succeeds cleanly (refreshing an already-fresh reserve rewrites the same numbers), so a bot on a schedule never needs attention and never fails spuriously.
- **Paid, if you need it reliable.** The strongest maintenance carries its own incentive, and Dave is the boundary case. He is not a crank operator paid per call: he is a principal who repays someone else's debt from his own pocket and takes their collateral at a discount, risking his own capital on prices holding while he does it. The market stays solvent because that job pays well enough to be worth the risk.
@@ -115,7 +115,7 @@ Dave is one instance of a pattern this market runs on. Some work in a financial
Five weeks in, Bob's cash crunch has passed. His $750 debt has become about $758 through the factor. He runs `repay_obligation_liquidity()` (anyone may repay any obligation, so there is no owner check; over-repayment clamps to the debt, and any sub-unit of principal stays with the borrower rather than being forgiven by rounding). Then `withdraw_obligation_collateral()` releases his NVDAx shares (the withdraw math rounds *up* the borrow power being removed, so a withdrawal can never slip past the health check by a rounding sub-unit), and `redeem_reserve_collateral()` turns shares back into his five NVDAx. Total cost of five weeks' liquidity: about eight dollars, and he never sold his NVIDIA.
-Maria runs `collect_protocol_fees()` whenever she likes and receives the accumulated 10% of interest, about 78 cents (`NothingToCollect` guards the empty case, and `protocol_fees_accrue_and_owner_can_collect` covers the flow). Of every dollar of interest Bob paid, ninety cents went to the suppliers, ten to the house.
+Maria runs `collect_program_fees()` whenever she likes and receives the accumulated 10% of interest, about 78 cents (`NothingToCollect` refuses a collection when nothing is owed, and `program_fees_accrue_and_owner_can_collect` covers the flow). Of every dollar of interest Bob paid, ninety cents went to the suppliers, ten to the house.
Alice leaves her SPCXx position in place and takes her USDC yield: `redeem_reserve_collateral()` burns her 1,000 shares for about $1,003.50, her half of the supplier interest, since she supplied half the USDC reserve alongside Carol's collateral deposit.
@@ -129,9 +129,17 @@ The final balances are:
## The Math: Two Rounding Helpers and a Rule
-Every number in this program flows through `mul_div_floor()` or `mul_div_ceil()`, u128 in, multiply before divide, checked everywhere. The rule deciding which to use: **quantities favorable to the user round down, debt and house-owed quantities round up.** Shares minted: floored. Redemptions: floored. Collateral value: floored. Debt: ceiled. Health checks: collateral floored, debt ceiled, doubly conservative. The payoff is an invariant the tests can check: the market never loses a minor unit to rounding, so dust cannot be extracted by repeated round trips, and the test `deposit_redeem_round_trip_creates_no_value` proves it by trying.
+Every number in this program flows through `mul_div_floor()` or `mul_div_ceil()`, u128 in, multiply before divide, checked everywhere. The rule deciding which to use: **quantities favorable to the user round down, debt and house-owed quantities round up.**
-## The Invariants: Solvency by Construction
+- **Shares minted:** floored.
+- **Redemptions:** floored.
+- **Collateral value:** floored.
+- **Debt:** ceiled.
+- **Health checks:** collateral floored and debt ceiled, so both roundings count against the borrower.
+
+The payoff is an invariant the tests can check: the market never loses a minor unit to rounding, so dust cannot be extracted by repeated round trips, and the test `deposit_redeem_round_trip_creates_no_value` checks it by trying.
+
+## The Invariants: What Keeps the Market Solvent
- Total liquidity = available + borrowed-with-interest − the house's accumulated fees, and the share supply plus the 1,000 withheld shares is a claim on exactly that; the exchange rate cannot be moved by donating tokens to the vault (checked by `raw_token_donation_does_not_inflate_exchange_rate`).
- No action may leave an obligation past its allowed borrow value, and every valuation is fresh (same transaction) or refused.
@@ -141,30 +149,30 @@ Every number in this program flows through `mul_div_floor()` or `mul_div_ceil()`
## The Failure Modes: The Liquidation Engine Is Only as Good as Its Prices
-- **A wrong price makes every number wrong.** Every health check multiplies by the feed, so a bad price means wrong collateral values, wrong debts, and liquidations that should not happen. This program refuses stale prices (`borrow_with_stale_price_feed_is_rejected`) and gates who may write them, but the lesson stands: the liquidation engine can only be as good as its prices. Manipulated prices have produced nine-figure losses on real markets (the price-manipulation section of the *Adversary* chapter dissects Mango Markets).
+- **A wrong price makes every number wrong.** Every health check multiplies by the feed, so a bad price means wrong collateral values, wrong debts, and liquidations that should not happen. This program refuses stale prices (`borrow_with_stale_price_feed_is_rejected`) and lets only the market owner write them (`non_owner_cannot_write_market_price_feed`), but the lesson stands: the liquidation engine can only be as good as its prices. Manipulated prices have produced nine-figure losses on real markets (the price-manipulation section of the *Adversary* chapter dissects Mango Markets).
- **Skipping the refresh** would let positions be judged on old values; `ReserveStale`, `ObligationStale`, and `borrow_without_obligation_refresh_is_rejected` close the path.
- **Cross-market smuggling**: an obligation in one market pledging reserves from another is refused (`MarketMismatch`, `cross_market_reserve_is_rejected`).
- **Dangerous configurations are rejected at write time**: LTV above threshold, zero close factor, misordered rate curves (`rejects_ltv_above_liquidation_threshold`, `rejects_misordered_interest_rate_curve`).
-- **Share inflation through the reserve's own borrowing.** Donating tokens to the vault moves nothing, because `available_liquidity`, not the vault balance, is the source of truth. But total liquidity also counts interest owed on borrows, and a supplier may borrow from the reserve they supply. The attacker is the only supplier of an empty reserve, holding a single minor unit of its shares, and borrows one minor unit from it against collateral in another reserve. Debt rounds up, so a second of interest makes that debt two minor units, and the attacker's unit of shares is worth two without anything being minted. From there, deposits and redemptions push the share price higher, because both round in the pool's favor and the attacker owns the whole pool. Once that unit is worth about 690 USDC, a 1,000 USDC deposit mints one unit of shares, and the attacker's unit redeems half the pool. `inflating_shares_through_own_borrow_does_not_pay` runs this on the default rate curve, and the attacker repays the loan and leaves no bad debt. Without the withheld shares the attacker turns 4,000 USDC into 4,155.20. With them, the attacker's unit is one of 1,001, and nearly everything the rounding leaves behind goes to shares nobody can redeem. The attacker ends 1,065 minor units down, and the victim gets back the whole deposit. A reserve whose suppliers have all left keeps the 1,000 minor units behind the withheld shares, and the next deposit is priced against them instead of starting the reserve over (`sole_supplier_leaves_the_minimum_behind`).
+- **Share inflation through the reserve's own borrowing.** Donating tokens to the vault moves nothing, because the program prices shares from `available_liquidity`, not the vault balance. But total liquidity also counts interest owed on borrows, and a supplier may borrow from the reserve they supply. The attacker is the only supplier of an empty reserve, holding a single minor unit of its shares, and borrows one minor unit from it against collateral in another reserve. Debt rounds up, so a second of interest makes that debt two minor units, and the attacker's unit of shares is worth two without anything being minted. From there, deposits and redemptions push the share price higher, because both round in the pool's favor and the attacker owns the whole pool. Once that unit is worth about 690 USDC, a 1,000 USDC deposit mints one unit of shares, and the attacker's unit redeems half the pool. `inflating_shares_through_own_borrow_does_not_pay` runs this on the default rate curve, and the attacker repays the loan and leaves no bad debt. Without the withheld shares the attacker turns 4,000 USDC into 4,155.20. With them, the attacker's unit is one of 1,001, and nearly everything the rounding leaves behind goes to shares nobody can redeem. The attacker ends 1,065 minor units down, and the victim gets back the whole deposit. A reserve whose suppliers have all left keeps the 1,000 minor units behind the withheld shares, and the next deposit is priced against them instead of starting the reserve over (`sole_supplier_leaves_the_minimum_behind`).
- **Self-liquidation** is allowed, deliberately: it is only possible while unhealthy and is economically pointless, matching how Save and Kamino Lend behave. Not every strange-looking path needs a check; this one is harmless because it never pays.
## Testing Considerations
-The suite is organized by risk, one file per class. Rounding comes first: the round-trip-creates-no-value invariant, and the withdraw-at-the-boundary-then-one-more-unit test, which is the "test both sides of every boundary" rule. Then deposits and redemptions, including the share-inflation attack and a first deposit too small to clear the withheld shares. Then interest over time (moving the clock forward to make weeks pass), liquidation (healthy rejected, unhealthy corrected with bonus and close factor, over-seizure refused), borrowing (every rejection path), and security (cross-market and unauthorized-price attempts). All of it drives the program's own handlers, with each action bundling its refresh instructions into the same transaction as a production client would.
+The suite is organized by risk, one file per class. Rounding comes first: the round-trip-creates-no-value invariant, and the withdraw-at-the-boundary-then-one-more-unit test, which tests both sides of the boundary. Then deposits and redemptions, including the share-inflation attack and a first deposit too small to clear the withheld shares. Then interest over time (moving the clock forward to make weeks pass), liquidation (healthy rejected, unhealthy corrected with bonus and close factor, over-seizure refused), borrowing (every rejection path), and security (cross-market and unauthorized-price attempts). All of it drives the program's own handlers, with each action bundling its refresh instructions into the same transaction as a production client would.
## What You Learnt from Building the Lending Market
-Strip the branding from a commercial bank and what remains is: pooled deposits earning a rate, loans against collateral marked to market, a margin call when the mark moves, and a spread for the house. This chapter built all four out of accounts at PDAs, one growing index, two rounding helpers, and a price that is true only because a named key signed it recently. The loan officer became a loan-to-value check, the risk committee became a config validator that refuses dangerous parameters, the margin clerk became any bot willing to claim a liquidation bonus, and the bank's interest ledger became a single number that only grows.
+Strip the branding from a commercial bank and what remains is: pooled deposits earning a rate, loans against collateral marked to market, a margin call when the mark moves, and a spread for the house. This chapter built all four out of accounts at PDAs, one growing index, two rounding helpers, and a price the program accepts only because the market owner's private key signed it recently. The loan officer became a loan-to-value check, the risk committee became a config validator that refuses dangerous parameters, the margin clerk became any bot willing to claim a liquidation bonus, and the bank's interest ledger became a single number that only grows.
Six concepts came out of the lending market:
- **Shares as the yield mechanism.** Interest grows the pool while the share supply is unchanged, so each share redeems for more; no per-lender bookkeeping exists or needs to.
- **The accumulation factor.** Store each borrower's principal and one shared factor, and compute a debt by multiplying them. The same structure prices any claim on a growing pool.
-- **Conservative-by-construction valuation.** Collateral floors, debt ceils, so every health check errs against the borrower, and no rounding sub-unit can slip a position past a boundary.
-- **Liquidation as a paid correction.** The close factor caps each call at part of the debt, so the borrower keeps what health does not require; the bonus pays the liquidator; the health gate means only positions already past the threshold can be touched.
+- **Valuation that rounds against the borrower.** Collateral floors, debt ceils, so every health check errs against the borrower, and no rounding sub-unit can slip a position past a boundary.
+- **Liquidation as a paid correction.** The close factor caps each call at part of the debt, so the borrower keeps what health does not require; the bonus pays the liquidator; `ObligationHealthy` rejects any position not already past the threshold.
- **Cranks.** Permissionless, idempotent maintenance whose funds destinations come from stored state, and whose reliability comes from someone being paid.
-- **Freshness as a gate.** Every valuation requires same-transaction refreshes, so acting on stale state is a transaction that cannot be built.
+- **Refreshes in the same transaction.** Every handler that values anything fails with `ReserveStale` or `ObligationStale` unless `refresh_reserve()` and `refresh_obligation()` ran earlier in the same transaction, so no handler acts on stale values.
-In the *Managed Fund* chapter we will build a fund: investors deposit into a pool of assets, and a manager trades that pool on their behalf. The lender in this chapter protects itself from its borrowers. The fund has to protect its investors from its own manager, and you will see what the manager is allowed to do with their money and what stops them doing anything else.
+In the *Managed Fund* chapter we will build a fund: investors deposit into a pool of assets, and a manager trades that pool on their behalf. The lender in this chapter protects itself from its borrowers. The fund has to protect its investors from its own manager, and you will see what the manager is allowed to do with their deposits and what stops them doing anything else.
[^lending-size]: [defillama.com/chain/solana](https://defillama.com/chain/solana) ranks Solana's categories by the value locked in them. Where lending sits in that ranking moves with the market.
diff --git a/building-financial-products-in-solana/managed-fund.md b/building-financial-products-in-solana/managed-fund.md
index 2dcd01bf..19c70dc2 100644
--- a/building-financial-products-in-solana/managed-fund.md
+++ b/building-financial-products-in-solana/managed-fund.md
@@ -2,13 +2,13 @@
A **managed fund** is the onchain equivalent of a mutual fund. A list of approved assets, a fund manager who picks the allocation, investors who deposit and receive shares, a management fee for the manager's expertise. Symmetry and Kamino are popular platforms for managed funds on Solana. The program lives at `finance/managed-fund` in the companion repository, and its story trades stocks: TSLAx and NVDAx, quoted in USDC.
-In a traditional fund, "the manager cannot run off with client money" is enforced by custodians, auditors, and courts. Here it is enforced by the program: **the manager can change the allocation and earn the fee, and can do nothing else.** No instruction in the program moves a vault's tokens to the manager.
+In a traditional fund, "the manager cannot run off with client assets" is enforced by custodians, auditors, and courts. Here it is enforced by the program: **the manager can change the allocation and earn the fee, and can do nothing else.** No instruction in the program moves a vault's tokens to the manager.
> **Also called a vault strategy.** Solana platforms often sell this product as a *vault* or a *vault strategy*, and the companion repository called its example `vault-strategy` before it took the finance name. This book uses the finance name because "vault" already means something here. On Solana, a **vault** is a token account owned by a program, and that is how every chapter uses the word. So **the product is the fund, and the token accounts are its vaults.** Nor is it an index fund, though the two look alike: an index fund's weights follow a published index with no manager choosing them, and here a manager chooses them.
## A Curator Decides What Managers May Touch at All
-Two different people hold power in this program, and their separation is its first security decision. Victor runs the registry: he is not a fund manager, but the curator who decides which assets managers may invest in at all, and which price feed is the truth for each. He operates the platform the funds live on, and his business is his reputation for funds not failing, so vetting feeds is his product. Maria, the fund manager you will meet shortly, gets to pick allocations only from what Victor has approved.
+Two different people hold power in this program, and their separation is its first security decision.[^separate-people] Victor runs the registry: he is not a fund manager, but the curator who decides which assets managers may invest in at all, and which Pyth price feed the program reads for each. He operates the platform the funds live on, and his business is his reputation for funds not failing, so vetting feeds is part of what he sells. Maria, the fund manager you will meet shortly, gets to pick allocations only from what Victor has approved.
Victor runs `initialize_registry()` once, creating the **registry**: the curator record at the root of an approved-asset set. The registry holds no list; the account stores only the curator's authority. Victor then runs `approve_asset()` for each asset he vets, and each call creates an **`ApprovedAsset`** account at a PDA derived from the registry and the asset's mint (seeds: `"approved_asset"`, the registry, the mint), binding that mint to its official Pyth price feed. An asset is approved if it has an `ApprovedAsset` account, and not approved if it does not. Victor vets which assets and which feeds are safe, so a manager cannot list a token they minted themselves, and cannot pair a real asset with a price feed they control. When Maria later adds an asset to her fund, the feed address is copied out of the `ApprovedAsset` account; the manager never supplies a price feed.
@@ -18,9 +18,11 @@ The approved set is stocks, bonds, whatever investors actually want. It cannot i
## The Manager Creates the Fund and Sets the Weights
-Maria is the fund manager: she creates the fund, sets the weights, rebalances, and earns the fee. She runs `initialize_fund()` with index 0, her fee of 100 basis points (1%), a `max_slippage_bps` of 100, and the swap router the fund will trade through. Both the fee and the slippage tolerance are capped at 10%: the fee is collected by dilution (below), so an uncapped fee would let a manager drain the fund *by configuration*, and the cap is the program refusing to make that a parameter.
+Maria is the fund manager: she creates the fund, sets the weights, rebalances, and earns the fee. She runs `initialize_fund()` with index 0, her fee of 100 basis points (1%), a `max_slippage_bps` of 100, and the swap router the fund will trade through. The fee is capped at 10%. It is collected by dilution (below), so an uncapped fee would let a manager drain the fund *by configuration*, and `initialize_fund()` rejects a higher fee with `FeeTooHigh`.
-She then runs `add_asset()` twice: TSLAx at 4,000 basis points, NVDAx at 6,000. Weights may not overflow 100% (`WeightOverflow`), and **nobody can deposit until the weights sum to exactly 100%** (`FundNotFullyAllocated`). A deposit into this fund is always fully invested, immediately, at target weights. There is no idle-cash mode, no "the manager will get to it." Maria can change weights later with `set_weight()`, and only Maria: the manager gate is checked by `test_set_weight_rejects_non_manager`.
+The slippage tolerance is capped at 10% too. It sets how far below the oracle price the swaps in a deposit or a rebalance may fill, so an uncapped tolerance would let a manager sell the fund's assets far below what they are worth. `initialize_fund()` rejects a higher tolerance with `SlippageConfigTooHigh`.
+
+She then runs `add_asset()` twice: TSLAx at 4,000 basis points, NVDAx at 6,000. Weights may not overflow 100% (`WeightOverflow`), and **nobody can deposit until the weights sum to exactly 100%** (`FundNotFullyAllocated`). A deposit into this fund is always fully invested, immediately, at target weights. There is no idle-cash mode, no "the manager will get to it." Maria can change weights later with `set_weight()`, and only Maria: `set_weight()` requires the fund's `manager` to sign, and `test_set_weight_rejects_non_manager` checks that anyone else is refused.

@@ -30,7 +32,7 @@ The **fund** account (PDA seeds: `"fund"`, an index) records the `manager`, the
Each basket asset gets an **asset config** account (PDA seeds: `"asset"`, the fund, an index byte) recording its mint, its feed (copied from its `ApprovedAsset` account), its weight in basis points, and its **vault**, a token account whose authority is the fund PDA. The fund holds up to `MAX_ASSETS` (16), and the cap is not about storage. A deposit has to reference every asset's accounts at once, five per asset, so a full basket runs to 94 accounts. That fits inside Solana's account-lock limit, but it is far past what a legacy transaction can hold, so clients of big baskets send v0 transactions with an address lookup table.
-The indexes guard the valuation: every handler that computes the fund's value walks indexes 0 through `asset_count` and refuses to run if any account is missing (`IncompleteAssetAccounts`), which makes it structurally impossible to omit an asset and understate the fund's value. A retired asset (weight set to zero) keeps its index forever rather than being reused, so the contiguous range the valuation loop depends on stays intact.
+Every handler that computes the fund's value walks indexes 0 through `asset_count` and refuses to run if any account is missing (`IncompleteAssetAccounts`), so a caller cannot leave an asset out and understate the fund's value. A retired asset (weight set to zero) keeps its index forever rather than being reused, so the contiguous range the valuation loop depends on stays intact.
## A Deposit Makes the Program Acquire Assets
@@ -38,7 +40,7 @@ Alice invests from the standard wallet of one SOL and 1,000 USDC. She runs `depo
**First it values the fund.** It walks every asset: reads the holding the program has recorded for that vault, loads the Pyth price, and accumulates value in u128 as amount × price ÷ 10^8, multiply before divide. The price load rejects a feed that is not the one recorded in the asset config, a price that is not positive, a price older than 60 seconds, or a price posted before the cluster's last restart. The recorded USDC is added to the total. That total is the **net asset value**, the NAV. The program keeps these records itself and updates them with what each transfer actually moves; the failure-modes section says why it never values the fund from its vaults' token balances. On the fund's first deposit the NAV is zero and shares are minted one-to-one: Alice's 900 USDC becomes 900 shares.
-**Then it deploys the money at the weights.** 40% of 900, $360, buys TSLAx at $250: 1.44 TSLAx into the TSLAx vault. 60%, $540, buys NVDAx at $180: exactly 3 NVDAx. The swaps run through the router fixed at fund creation (the example ships a mock router standing in for Jupiter; a router that is not the configured one is rejected, checked by `test_deposit_rejects_unregistered_router`), and the fund PDA signs. The handler measures what each swap moved on the vaults and adds it to the recorded holdings. A swap that returns none of its asset refuses the whole deposit (`DepositTooSmall`).
+**Then it deploys the USDC at the weights.** 40% of 900, $360, buys TSLAx at $250: 1.44 TSLAx into the TSLAx vault. 60%, $540, buys NVDAx at $180: exactly 3 NVDAx. The swaps run through the router fixed at fund creation (the example ships a mock router standing in for Jupiter; a router that is not the configured one is rejected, checked by `test_deposit_rejects_unregistered_router`), and the fund PDA signs. The handler measures what each swap moved on the vaults and adds it to the recorded holdings. A swap that returns none of its asset refuses the whole deposit (`DepositTooSmall`).
**And the program, not the manager, decides the acceptable price.** For each leg, the handler computes the expected output *from the oracle price* and floors it by the fund's slippage tolerance: the swap may return at most 1% less than the feed says it should, or the whole deposit reverts. A manager cannot approve a bad fill, because the minimum is never theirs to supply. **When an untrusted party routes the trade, the acceptable outcome must be computed from a price source they do not control.**
@@ -46,19 +48,19 @@ Alice invests from the standard wallet of one SOL and 1,000 USDC. She runs `depo
## The Market Moves and the Manager Rebalances
-NVDAx climbs from $180 to $200. The fund is now worth $960, so each share is worth about $1.07, and nothing needs to happen for that to be true: the share price *is* NAV divided by supply, recomputed from feeds every time it matters.
+NVDAx climbs from $180 to $200. The 1.44 TSLAx and 3 NVDAx in the fund's vaults are now worth $960, and 900 shares exist, so each share is worth about $1.07. The program does not store a share price; each deposit divides the NAV, valued from the feeds at that moment, by the share supply.
-But the weights have drifted: NVDAx's gain pushed it to 62.5% of the fund against a 60% target. This is Maria's job. She runs `rebalance()`, which sells the over-weighted asset for USDC and buys the under-weighted one: 0.12 NVDAx sold for $24, $24 of TSLAx bought, both legs through the fixed router, both legs floored by oracle-anchored minimums as deposits are. The weights return to 40/60. The money moved between fund-owned vaults, through the approved router, at oracle-checked prices, and nowhere near Maria.
+But the weights have drifted: NVDAx's gain pushed it to 62.5% of the fund against a 60% target. This is Maria's job. She runs `rebalance()`, which sells the over-weighted asset for USDC and buys the under-weighted one: 0.12 NVDAx sold for $24, $24 of TSLAx bought, both legs through the fixed router, both legs floored by oracle-anchored minimums as deposits are. The weights return to 40/60. The tokens moved between fund-owned vaults, through the approved router, at oracle-checked prices, and nowhere near Maria.

## A Later Investor Buys In at the New Price
-Bob, the second investor, deposits 480 USDC of his standard wallet. He is not early, and the share math prices that in: shares = deposit × total shares ÷ NAV, floored, with the NAV valued from the recorded holdings. With 900 shares outstanding against a $960 NAV, his 480 buys 450 shares, not 480. Alice's early position is not diluted by his arrival, and the test suite checks the fairness directly (`test_deposit_fair_pricing`). His money deploys 40/60 at current prices, same as hers, same slippage floor.
+Bob, the second investor, deposits 480 USDC of his standard wallet. He is not early, and the share math prices that in: shares = deposit × total shares ÷ NAV, floored, with the NAV valued from the recorded holdings. With 900 shares outstanding against a $960 NAV, his 480 buys 450 shares, not 480. Alice's early position is not diluted by his arrival, and the test suite checks the fairness directly (`test_deposit_fair_pricing`). His USDC deploys 40/60 at current prices, same as hers, same slippage floor.
## The Manager's Fee Arrives by Dilution
-Traditional funds sell assets to pay the management fee. This program does what large onchain fund products do instead: it **mints new shares to the manager**, so the fee is paid by diluting every holder proportionally, with no asset ever sold. `collect_fees()` computes shares owed as total shares × fee rate × elapsed time over a year, from the fund's fee clock: at 1% with 1,350 shares outstanding and a year elapsed, 13.5 new shares to Maria. The instruction is deliberately callable by anyone (the fee accrues by time, not by the manager's diligence in claiming it), and the fee rate itself has no setter: it is fixed at fund creation, so the deal investors joined under cannot be worsened. The *Betting Market* chapter snapshotted a fee per event; the same principle, at fund scale.
+Traditional funds sell assets to pay the management fee. This program does what large onchain fund products do instead: it **mints new shares to the manager**, so the fee is paid by diluting every holder proportionally, with no asset ever sold. `collect_fees()` computes shares owed as total shares × fee rate × elapsed time over a year, from the fund's fee clock: at 1% with 1,350 shares outstanding and a year elapsed, 13.5 new shares to Maria. The instruction is deliberately callable by anyone (the fee accrues by time, not by the manager's diligence in claiming it), and the fee rate itself has no setter: it is fixed at fund creation, so the deal investors joined under cannot be worsened. The *Betting Market* chapter stored each event's fee when the event was created, for the same reason.

@@ -72,28 +74,35 @@ She entered with $900, rode NVIDIA up, paid Maria 1% by dilution, and left with
## The Math: One Valuation Loop, Floors Toward the Fund
-Every formula in the program is a proportion, and they all round the same way. Shares minted: deposit × supply ÷ NAV, floored. Withdrawal legs: recorded holding × shares ÷ supply, floored per asset, with each payout subtracted from the record. Fee shares: supply × rate × time over a year, floored. Deployment per leg: deposit × weight ÷ 10,000, floored. All u128 intermediates, all multiply-before-divide, remainders staying with the fund rather than the transactor. The one multiplication that is not a proportion is the valuation itself, amount × price at the feed's 10^8 precision, and the fund's integrity depends on that line running over every asset, every time, with a fresh price.
+Every formula in the program is a proportion, and they all round the same way:
+
+- **Shares minted:** deposit × supply ÷ NAV, floored.
+- **Withdrawal legs:** recorded holding × shares ÷ supply, floored per asset, with each payout subtracted from the record.
+- **Fee shares:** supply × rate × time over a year, floored.
+- **Deployment per leg:** deposit × weight ÷ 10,000, floored.
-## The Invariants: Custody Never Moves
+All of them use u128 intermediates and multiply before dividing, and the remainders stay with the fund rather than the transactor. The one multiplication that is not a proportion is the valuation itself, amount × price at the feed's 10^8 precision, and the share price is right only if that line runs over every asset, on every deposit, with a fresh price.
+
+## The Invariants: Assets Stay in the Fund's Vaults
- From first deposit to last withdrawal, assets live only in fund-owned vaults. No instruction handler transfers vault tokens to the manager; the fee path mints shares and touches no vault.
- Deposits are accepted only at 100% allocation, and every deposit is fully deployed in the same transaction.
- NAV computations include every asset index or refuse to run.
-- Share supply changes only through deposit, withdrawal, and the time-based fee, and `total_shares` moves before the corresponding CPIs.
+- Share supply changes only through `deposit()`, `withdraw()`, and `collect_fees()`, and `total_shares` moves before the corresponding CPIs.
- Every swap leg, deposit or rebalance, clears at no worse than the oracle price minus the configured tolerance, through the router fixed at creation.
- Every share price and every payout comes from the recorded holdings, never from a vault's token balance. The recorded holdings never exceed the balances, and they equal them unless someone has transferred tokens in directly.
## The Failure Modes: The Manager, the Feed, and the First Deposit
-- **The manager is the threat model**, and the program treats them as such: assets only from Victor's list, feeds only from Victor's entries, swaps only through the fixed router, minimums only from the oracle, fee capped and immutable, custody never theirs. What remains to a careless manager is churn: pointless rebalancing that pays market slippage. That hurts depositors, but it does not enrich the manager, which is the design's limit. Aligning *that* incentive is what the fee-on-assets structure is for.
+- **The manager is included in the threat model**, and the program treats them as such: assets only from Victor's list, feeds only from Victor's entries, swaps only through the fixed router, minimums only from the oracle, fee capped and immutable. No handler sends vault tokens to them. What remains to a careless manager is churn: pointless rebalancing that pays market slippage. That hurts depositors but pays the manager nothing, and the program cannot tell a pointless rebalance from a useful one. Aligning *that* incentive is what the fee-on-assets structure is for.
- **Every number in the fund depends on the feed.** A stale price is refused (`StalePriceFeed`, 60-second window), as is a non-positive price (`NegativePrice`) and a substituted feed account (`InvalidPriceFeed`), so no deposit, withdrawal, or rebalance runs on a bad price. A price posted at or before the cluster's last restart is refused too (`PricePredatesRestart`). After a halt the timestamp trails real time, so a price from before the halt can still look under a minute old, and the *Time* chapter explains why. What this example does not check is the feed's confidence interval; a production fund should, and the *Offchain Truth* chapter explains what confidence means and why "fresh but uncertain" is its own failure mode.
- **Donations are ignored, which closes the inflation attack.** Anyone can transfer tokens straight into a vault. A program that valued the fund from its vaults' balances would let a one-minor-unit first deposit and a donation price one share above the next deposit, rounding it to zero shares. This program values the fund from `usdc_holdings` and `asset_holdings`, so a donation changes a balance and nothing the handlers read. Run the *Adversary* chapter's numbers against it: after the attacker's one minor unit and 1,000 USDC donation, the next depositor's 1,000 USDC still buys 1,000 shares. The donation stays in the vault, and no withdrawal ever pays it out. `rebalance` cannot sell or spend donated tokens either (`InsufficientHoldings`).
-- **A deposit too small to buy anything is refused.** Ignoring donations leaves one gap: a deposit so small that a swap takes its USDC and returns none of the asset. It would mint shares against a fund recorded as worth nothing, and every later deposit would then divide by zero, so it fails with `DepositTooSmall`. The `minimum_shares` argument remains for what neither guard covers, a share price that moved between signing and landing. The empty-pool section of the *Adversary* chapter compares this defense with the others.
+- **A deposit too small to buy anything is refused.** Ignoring donations leaves one gap: a deposit so small that a swap takes its USDC and returns none of the asset. It would mint shares against a fund recorded as worth nothing, and every later deposit would then divide by zero, so it fails with `DepositTooSmall`. The `minimum_shares` argument remains for what neither check covers, a share price that moved between signing and landing. The empty-pool section of the *Adversary* chapter compares this defense with the others.
- **Partial baskets are refused**, on deposit, withdrawal, and valuation alike (`IncompleteAssetAccounts`): an attacker who could get the fund valued without its best-performing vault could buy shares at a discount.
## Testing Considerations
-The suite (with hand-built Pyth price-update accounts injected so prices are deterministic) covers the full lifecycle end to end (`test_full_lifecycle`). It then checks each rule separately. Under-allocated funds refuse deposits, unapproved assets refuse listing, weight overflows refuse, the 17th asset refuses, and non-managers cannot set weights. Unregistered routers refuse, slippage floors reject on deposit and withdrawal, incomplete account lists refuse, and later depositors pay fair NAV. `test_full_lifecycle` checks after every step that the recorded holdings equal the vaults' balances. `test_donation_does_not_inflate_share_price` runs the *Adversary* chapter's attack and checks that the victim gets exactly the shares they would have got without the donation. `test_deposit_rejects_leg_that_buys_nothing` and `test_rebalance_cannot_spend_donated_usdc` check the other two guards. `test_deposit_rejects_price_from_before_restart` simulates a halt, checks that a deposit is refused while the prices predate the restart, and checks that it goes through once Pyth posts again. The pattern to copy is the price injection: valuation code cannot be tested with prices you cannot control, so the harness makes the feed a fixture and then moves it deliberately.
+The suite (with hand-built Pyth price-update accounts injected so prices are deterministic) covers the full lifecycle end to end (`test_full_lifecycle`). It then checks each rule separately. Under-allocated funds refuse deposits, unapproved assets refuse listing, weight overflows refuse, the 17th asset refuses, and non-managers cannot set weights. Unregistered routers refuse, slippage floors reject on deposit and withdrawal, incomplete account lists refuse, and later depositors pay fair NAV. `test_full_lifecycle` checks after every step that the recorded holdings equal the vaults' balances. `test_donation_does_not_inflate_share_price` runs the *Adversary* chapter's attack and checks that the victim gets exactly the shares they would have got without the donation. `test_deposit_rejects_leg_that_buys_nothing` and `test_rebalance_cannot_spend_donated_usdc` test the other two checks. `test_deposit_rejects_price_from_before_restart` simulates a halt, checks that a deposit is refused while the prices predate the restart, and checks that it goes through once Pyth posts again. The pattern to copy is the price injection: valuation code cannot be tested with prices you cannot control, so the harness makes the feed a fixture and then moves it deliberately.
## What You Learnt from Building the Managed Fund
@@ -101,10 +110,12 @@ A mutual fund needs a custodian, a compliance officer, a fee schedule in a prosp
The managed fund added five concepts:
-- **The manager as the threat model.** The invariant every program in this book keeps: no instruction handler, under any parameters, moves user deposits to the operator. Rebalancing moves tokens between program-owned vaults; the fee path mints shares and touches no vault.
+- **The manager included in the threat model.** The invariant every program in this book keeps: no instruction handler, under any parameters, moves user deposits to the operator. Rebalancing moves tokens between program-owned vaults; the fee path mints shares and touches no vault.
- **A curator separate from a manager.** Victor's approved-asset set binds each asset to its official feed, so a manager can neither list their own token nor pair a real asset with a feed they control.
- **Oracle-anchored minimums.** When an untrusted party routes the trade, the acceptable outcome must be computed from a price source they do not control.
-- **NAV as the share price, from the program's own records.** Value every holding with a fresh feed or refuse to run; a valuation that can skip an asset is a discount for whoever exploits it. A valuation read off token balances can be moved by anyone who transfers tokens in, so value what the program recorded.
-- **Fees by dilution, snapshotted.** The fee mints shares against everyone proportionally, accrues by time rather than the manager's diligence, and has no setter, so the deal investors joined under cannot be worsened.
+- **NAV as the share price, from the program's own records.** Value every holding with a fresh feed or refuse to run; a valuation that skips an asset lets a depositor buy shares for less than they are worth. A valuation read off token balances can be moved by anyone who transfers tokens in, so value what the program recorded.
+- **Fees by dilution, at a rate fixed at creation.** The fee mints shares against everyone proportionally, accrues by time rather than the manager's diligence, and has no setter, so the deal investors joined under cannot be worsened.
In the *Options* chapter we will build a program that takes no credit risk at all. A writer posts the whole of what they might owe before the option is sold, so there is no margin, no liquidator, and no oracle, and you will see how much simpler the program is for it. The *Perpetual Futures* chapter then closes the book with a program that needs all of it at once: vaults, shares, oracles, funding, margin, and liquidation.
+
+[^separate-people]: The program sees two public keys, not two people, and nothing onchain stops one person holding both the curator's and the manager's private keys. Keeping Victor and Maria separate people needs proof from outside the program, from a proof-of-personhood service like those in the *Fundraiser* chapter's footnote.
diff --git a/building-financial-products-in-solana/options.md b/building-financial-products-in-solana/options.md
index 223a0bf3..295712b7 100644
--- a/building-financial-products-in-solana/options.md
+++ b/building-financial-products-in-solana/options.md
@@ -31,7 +31,7 @@ Every option has exactly two endings, and the price at expiry decides which: the
Traditional options venues let a writer take on that obligation with only a fraction of it posted as **margin**, and then watch the position every day to make sure the fraction still covers it. This program does something simpler. **A writer posts the whole obligation, up front, into the vault.**
- A call writer supplies the underlying asset to the program, so the call is **covered**: the shares the holder might buy are already in custody, and the writer cannot fail to deliver them.
-- A put writer supplies the program with enough of the quote token to buy every share at the strike price, so the put is **cash-secured**: the money the holder might be paid is already in custody, and the writer cannot fail to pay.
+- A put writer supplies the program with enough of the quote token to buy every share at the strike price, so the put is **cash-secured**: the quote tokens the holder might be paid are already in custody, and the writer cannot fail to pay.
Nothing in this program is ever undercollateralized, so it has no margin, no health factor, no liquidator, and no oracle.
@@ -53,7 +53,7 @@ Maria runs the venue. Her incentive is a fee of 1% on every premium paid, and he
- A venue whose underlying and quote are the same token.
- A fee of 100% or more, which would leave a writer nothing from a sale.
-A fee of zero is allowed: a venue run at cost is a valid choice, and `test_zero_fee_venue_pays_the_writer_the_whole_premium` checks that the fee transfer is then skipped rather than sent for nothing. Her key is stored as the market's `admin`, and the only handler that reads it is `collect_fees()`.
+A fee of zero is allowed: a venue run at cost is a valid choice, and `test_zero_fee_venue_pays_the_writer_the_whole_premium` checks that the fee transfer is then skipped rather than sent for nothing. Her public key is stored as the market's `admin`, and the only handler that reads it is `collect_fees()`.

@@ -65,7 +65,7 @@ The **market** account lives at a PDA with seeds `"market"`, the underlying mint
- `quote_locked`: how much of the quote vault the market owes, as put writers' collateral and as call holders' strike payments waiting to be collected.
- `fees_owed`: how much of the quote vault belongs to the admin.
-The market PDA is the authority of the **underlying vault** and the **quote vault** (seeds `"underlying_vault"` and `"quote_vault"` + market), and the program signs for them with the market's seeds. Both vaults hold other people's money, mixed: the underlying vault holds every call writer's shares together, and the quote vault holds every put writer's cash, every exercised call's strike payment, and the venue's fees together. One rule keeps that safe: a vault balance is custody, and the three counters are the truth about who is owed what.
+The market PDA is the authority of the **underlying vault** and the **quote vault** (seeds `"underlying_vault"` and `"quote_vault"` + market), and the program signs for them with the market's seeds. Both vaults hold other people's tokens, mixed: the underlying vault holds every call writer's shares together, and the quote vault holds every put writer's cash, every exercised call's strike payment, and the venue's fees together. The three counters record how much of each vault is owed, and after every transfer `check_custody()` checks that each vault still holds at least that much.
## A Writer Posts the Whole Obligation and Names a Price
@@ -108,7 +108,7 @@ There is no strike *price* to multiply by a quantity and divide by a decimals fa
One more account is set up here. Alice's USDC account, where her premium will arrive, is created if it does not exist, paid for by Alice, so that `buy_option()` can require it to exist rather than make the buyer pay rent on an account that belongs to the writer.
-Nobody has paid anything yet. Until someone does, Alice can call `cancel_option()` at any time, which returns her collateral and closes the account, rent back to her. That includes after the expiry: an unsold option has no holder whose rights the cancel would cut short, and `test_cancel_unsold_option_works_after_expiry` checks it. Without this exit, an option nobody buys would hold the writer's shares forever.
+Nobody has paid anything yet. Until someone does, Alice can call `cancel_option()` at any time, which returns her collateral and closes the account, rent back to her. That includes after the expiry: an unsold option has no holder whose rights the cancel would cut short, and `test_cancel_unsold_option_works_after_expiry` checks it. If `cancel_option()` didn't exist, the shares a writer posted for an option nobody buys would stay locked in the market's vault forever.
## A Buyer Pays the Premium, and the Collateral Does Not Move
@@ -121,7 +121,7 @@ Bob calls `buy_option()`. The handler does four things:

-The premium is the only money that changes hands, and it is Alice's from this moment whatever Bob does later. The 5 NVDAx do not move. Bob's downside is fixed at the 25 USDC he just paid, and Alice's upside on the shares is capped at $180 until the week is out.
+The premium is the only payment in the purchase, and it is Alice's from this moment whatever Bob does later. The 5 NVDAx do not move. Bob's downside is fixed at the 25 USDC he just paid, and Alice's upside on the shares is capped at $180 until the week is out.
A writer cannot buy their own option. Their address would fill both the `buyer` and `writer` slots, and the premium's source and destination would be the same token account in two mutable slots, which the runtime rejects before the handler runs (`test_writer_cannot_buy_their_own_option`). A second buyer is refused because the option is no longer `Listed` (`test_buy_is_refused_once_sold`).
@@ -150,11 +150,11 @@ Alice calls `collect_proceeds()`. The handler requires the option to be `Exercis

-Alice sold her five NVDAx for 900 USDC and had already been paid 24.75 USDC for agreeing to. She holds 1,924.75 USDC, an effective $184.95 a share against the $200 they now trade at. The $15.05 a share she gave up is the writer's side of the four-ways figure, its last column: the premium was all she could make, and she made it.
+Alice sold her five NVDAx for 900 USDC and had already been paid 24.75 USDC for agreeing to. She holds 1,924.75 USDC, an effective $184.95 a share against the $200 they now trade at. The $15.05 a share she gave up is the writer's side of the four-ways figure, its last column: the premium was all she could make.
## A Put Writer Posts Cash, and the Option Expires Unexercised
-The put is the mirror image, and it also shows the other exit. Carol would like to own NVDAx at $150 and would like to be paid for standing ready to buy. She calls `write_option()` with kind `Put`, 5 contracts, 1 NVDAx per contract, a strike of 150 USDC per contract, a premium of 20 USDC, and an expiry a week out. A put writer's collateral is the strike, so 5 times 150 USDC, 750 USDC, leaves her account for the quote vault, and `quote_locked` rises to match. Her 1,000 USDC is 250.
+The put is the mirror image, and it also shows how an option ends when nobody exercises it. Carol would like to own NVDAx at $150 and would like to be paid for standing ready to buy. She calls `write_option()` with kind `Put`, 5 contracts, 1 NVDAx per contract, a strike of 150 USDC per contract, a premium of 20 USDC, and an expiry a week out. A put writer's collateral is the strike, so 5 times 150 USDC, 750 USDC, leaves Carol's account for the quote vault, and `quote_locked` rises to match. Her 1,000 USDC is 250.

@@ -182,7 +182,7 @@ Maria calls `collect_fees()`. The handler pays her `fees_owed`, 0.45 USDC across

-Everyone ends the story with what their position promised:
+Here is where everyone ends up:
- Alice earned a premium and sold her shares at her price.
- Bob turned 25 USDC of premium into five NVDAx at a $20 discount to the market.
@@ -200,15 +200,15 @@ The arithmetic lives in `contract_math.rs` and is the smallest in this book:
Every checked operation maps to `MathOverflow`. There is no rounding rule to choose anywhere in settlement, which is the payoff of storing the strike as an amount per contract rather than as a price.
-## The Invariants: The Vaults Cover the Ledger, and the Clock Picks One Exit
+## The Invariants: The Vaults Cover the Ledger, and Expiry Separates Exercise From Reclaim
- **Each vault covers what the market owes.** The underlying vault holds at least `underlying_locked`; the quote vault holds at least `quote_locked` plus `fees_owed`. Every handler that moves tokens updates the counters first and then calls `check_custody()` against the balances its transfers will leave behind, and fails with `CustodyInvariantViolated` if a refactor ever breaks the accounting. The tests strengthen it to equality after every step, because nothing in them donates to a vault.
-- **A held option has exactly one open exit at every instant.** Before `expiry` the holder can exercise and the writer cannot reclaim; at and after it, the reverse. There is no moment at which both can claim the same collateral and none at which neither can.
-- **Every exit closes the account.** Cancel, collect, and reclaim all end by closing the option account and returning its rent to the writer, so an option cannot be collected twice: the account is gone.
+- **At every instant, exactly one of `exercise_option()` and `reclaim_collateral()` accepts a held option.** Before `expiry` the holder can exercise and the writer cannot reclaim; at and after it, the reverse. There is no moment at which both can claim the same collateral and none at which neither can.
+- **`cancel_option()`, `collect_proceeds()`, and `reclaim_collateral()` all close the account.** Each ends by closing the option account and returning its rent to the writer, so an option cannot be collected twice: the account is gone.
- **The admin reaches only the fees.** `collect_fees()` moves `fees_owed` and nothing else; no handler moves collateral or a strike payment to the admin under any parameters.
- **Physical settlement moves exactly the posted terms.** What the writer collects is what the holder paid, and what the holder receives is what the writer posted, to the minor unit, for every option the program would accept.
-The companion repository's `kani-proofs` crate proves the last two and the first: `proof_vault_ledger_stays_consistent_across_every_lifecycle` writes two options of either kind into shared vaults, sends each down one of its three exits, and shows the ledger consistent after every step and both vaults empty at the end. `proof_exercise_and_reclaim_windows_partition_time` is the second invariant over every pair of timestamps.
+The companion repository's `kani-proofs` crate model-checks the last two and the first. `proof_vault_ledger_stays_consistent_across_every_lifecycle` writes two options of either kind into shared vaults, ends each with a cancel, a reclaim, or an exercise and collection, and shows the ledger consistent after every step and both vaults empty at the end. `proof_exercise_and_reclaim_windows_partition_time` checks the second invariant for every pair of timestamps.
## The Failure Modes: What Can Go Wrong When Nothing Can Be Liquidated
@@ -217,11 +217,11 @@ The companion repository's `kani-proofs` crate proves the last two and the first
- **A writer blocks the exercise.** If the strike were paid to the writer's token account inside `exercise_option()`, a writer could close that account and make every exercise fail, keeping the premium and the collateral both. The strike goes into the vault instead, owed to the writer, and the holder's exercise depends on no account of the writer's.
- **The wrong account carries the premium.** The buyer pays the writer directly, so the destination has to be bound to the writer or a buyer could name their own account and buy the option for free. The Anchor build derives it as the writer's associated token account; the Quasar build, whose token accounts are not associated token accounts, checks the account's owner and mint in the handler, and `buy_refuses_a_premium_account_the_writer_does_not_own` checks it.
- **What the design gives up.** A held option cannot be resold, and an option cannot be exercised in part. Venues that want both represent each option as two tokens, an option token the holder can sell on any exchange and a writer token that claims the proceeds. Adding that back means introducing the tokens, and with them an accounting of who holds how much of each option.
-- **What is absent.** Most of the usual attacks on a financial program return "not applicable" here. No oracle to lag or manipulate, because no price is read. No liquidation to front-run, because no position can be under water. No inflation attack, because there are no shares. No rounding to drain, because there is no division. The venue's entire risk surface is the custody ledger and the two comparisons against the clock, which is why both are proved rather than only tested.
+- **What is absent.** Most of the usual attacks on a financial program return "not applicable" here. No oracle to lag or manipulate, because no price is read. No liquidation to front-run, because no position can be under water. No inflation attack, because there are no shares. No rounding to drain, because there is no division. The venue's entire risk surface is the custody ledger and the two comparisons against the clock, which is why both are model-checked as well as tested.
## Testing Considerations
-The suite (`tests/test_options.rs`) follows the call from write to collected strike and the put from write to exercise and to expiry, asserting every balance to the minor unit, and it calls the custody check's equality form after every step. Around the happy paths there is one test per gate:
+The suite (`tests/test_options.rs`) follows the call from write to collected strike and the put from write to exercise and to expiry, asserting every balance to the minor unit, and it calls the custody check's equality form after every step. Around the happy paths there is one test per refusal:
- The expiry boundary, from both sides.
- Cancel after a sale, and buy after a sale or after expiry.
@@ -241,11 +241,11 @@ The options venue added six concepts:
- **Full collateralization removes whole categories.** A covered call and a cash-secured put cannot default, so the program needs no margin, no liquidator, and no oracle; everything the *Perpetual Futures* chapter adds is the cost of letting writers post less than they owe.
- **Physical settlement needs no price.** The holder's decision replaces the feed, and the program's job is to enforce the terms, not to value them.
- **Amounts as products, not prices.** Storing the strike as an amount per contract makes every settlement a checked multiplication, with no division and no rounding direction to defend.
-- **A ledger for mixed custody.** When one vault holds several parties' money, the state account counts what each is owed and every handler asserts the vault still covers it, after the math.
-- **A deadline is a comparison and its complement.** The holder's window and the writer's window partition time, tested from both sides and proved over every timestamp.
+- **A ledger for mixed custody.** When one vault holds several parties' tokens, the state account counts what each is owed and every handler asserts the vault still covers it, after the math.
+- **One deadline, compared both ways.** Exercise requires `now < expiry` and reclaim requires `now >= expiry`, so at every moment exactly one of them works, which the tests check from both sides and the Kani model checks cover every timestamp.
-In the *Perpetual Futures* chapter we will relax the collateral rule. Traders post a fraction of their exposure, an oracle values the rest continuously, and a liquidator is paid to close what the fraction no longer covers. Keeping such a venue solvent is hard, and you will see how much machinery it takes to handle positions whose collateral no longer covers what they owe.
+In the *Perpetual Futures* chapter we will relax the collateral rule. Traders post a fraction of their exposure, an oracle values the rest continuously, and a liquidator is paid to close what the fraction no longer covers. Keeping such a venue solvent takes a price check in every handler, liquidity reserved for every winner, and a liquidator paid to close positions whose collateral no longer covers what they owe.
-[^psyoptions]: PsyOptions pioneered physically settled, fully collateralized options on Solana, with no oracle, and this chapter's venue follows its settlement design. PsyOptions represented each option series as fungible SPL tokens, one mint per series; this program keeps one account per option instead, trading that composability for legible custody.
+[^psyoptions]: PsyOptions pioneered physically settled, fully collateralized options on Solana, with no oracle, and this chapter's venue follows its settlement design. PsyOptions represented each option series as fungible SPL tokens, one mint per series; this program keeps one account per option instead, trading that composability for a simpler custody ledger.
[^zeta]: Zeta Markets describes its margined, cash-settled design at [docs.zeta.markets](https://docs.zeta.markets). The comparison is between two settlement designs, not two products: a cash-settled venue can offer leverage and needs a price feed to do it, and a physically settled one offers neither.
diff --git a/building-financial-products-in-solana/order-book.md b/building-financial-products-in-solana/order-book.md
index 74e026d4..4a15d74e 100644
--- a/building-financial-products-in-solana/order-book.md
+++ b/building-financial-products-in-solana/order-book.md
@@ -1,8 +1,8 @@
# Chapter 14: The Order Book Exchange
-NYSE, NASDAQ, and the CME are order book exchanges. On Solana, Manifest, OpenBook, and Phoenix are order book exchanges, and by mid-2026 Manifest was carrying more spot volume than any other order book on the network.[^order-book-venues] All of them run the same machinery: orders arrive carrying a price their owner refuses to cross, wait in two sorted lists, trade when one order reaches across the gap, and settle out of vaults the venue's operator cannot reach.
+NYSE, NASDAQ, and the CME are order book exchanges. On Solana, Manifest, OpenBook, and Phoenix are order book exchanges, and by mid-2026 Manifest was carrying more spot volume than any other order book on the network.[^order-book-venues] All of them work the same way: orders arrive carrying a price their owner refuses to cross, wait in two sorted lists, trade when one order reaches across the gap, and settle out of vaults the venue's operator cannot reach.
-The program lives at `finance/order-book` in the companion repository. It assumes you know the escrow pattern from the *Escrow* chapter: tokens held in program-owned vaults, terms recorded in data accounts, and every movement gated by who signed. An exchange is that pattern scaled up from one deal to thousands of simultaneous ones.
+The program lives at `finance/order-book` in the companion repository. It assumes you know the escrow pattern from the *Escrow* chapter: tokens held in program-owned vaults, terms recorded in data accounts, and every movement allowed only when the right account signs. An exchange is that pattern scaled up from one deal to thousands of simultaneous ones.
## A Limit Order Is a Price You Refuse to Cross
@@ -18,7 +18,7 @@ One naming note: this design is traditionally called a *central limit* order boo
The first instinct is to store the bids and asks as sorted arrays. Arrays get slow when an exchange gets busy: inserting into the middle of a sorted list means shifting everything after it, and on a chain where compute is metered, your users pay that cost.
-So the book's two sides are **critbit trees**. Each resting order gets a fixed-size binary key, and the tree branches at the first bit where keys differ (the "critical bit"). Inserting an order means walking to where its key diverges and adding one node there; nothing shifts. The key is built from the price in the high bits and a sequence number in the low bits, which yields the exchange rule you want for free: walking the tree visits orders best price first, and within a single price, earliest order first. That ordering has a name, **price-time priority**, and it is the fairness rule the major exchanges use.
+So the book's two sides are **critbit trees**. Each resting order gets a fixed-size binary key, and the tree branches at the first bit where keys differ (the "critical bit"). Inserting an order means walking to where its key diverges and adding one node there; nothing shifts. The key is built from the price in the high bits and a sequence number in the low bits, which puts the tree in exchange order: walking it visits orders best price first, and within a single price, earliest order first. That ordering has a name, **price-time priority**, and it is the fairness rule the major exchanges use.
A critbit tree does not balance itself, so an attacker can place asks at 2, 4, 8, and so on to deepen the tree. That only adds a minor cost: 64 levels at most, and under 15,000 extra compute units per instruction, out of a default budget of 200,000.
@@ -32,7 +32,7 @@ Maria runs the exchange. She is the market authority: she creates the market and
Maria's client first creates the empty order book account, then calls `initialize_market()` with her chosen parameters: `fee_basis_points` (25), `tick_size`, `base_lot_size`, `quote_lot_size`, and `min_order_size`. The handler validates all of them: every size must be nonzero, and the fee is capped at 10,000 basis points, because a fee above 100% is nonsense. It creates the market account and the three vaults with the market as authority, records every address on the market account, and stamps the order book's first bytes so each tree knows which side it is.
-The tick size and minimum order size keep the book usable, and they do different jobs. Orders below `min_order_size` are rejected outright, and this is the gate that protects the book: every slot in the tree costs its occupant at least that much locked capital, so filling the tree costs real money. Prices must be a multiple of `tick_size` or the order is rejected, which keeps the book from fragmenting into thousands of dust price levels. The tick also sets the price of queue-jumping: a maker who wants to stand in front of the queue must improve the price by a whole tick, which is an improvement the taker actually receives.
+The tick size and minimum order size keep the book usable, and they do different jobs. Orders below `min_order_size` are rejected outright with `BelowMinOrderSize`, and this is what protects the book: every slot in the tree holds at least that much of its occupant's tokens in a vault, so filling the tree costs real capital. Prices must be a multiple of `tick_size` or the order is rejected, which keeps the book from fragmenting into thousands of dust price levels. The tick also sets the price of queue-jumping: a maker who wants to stand in front of the queue must improve the price by a whole tick, which is an improvement the taker actually receives.
Both are set per market, at creation, and cannot change afterward. A tick right for a $250 stock is far too coarse for a token priced at a fraction of a cent, so somebody has to choose one for each market, and the choice is final. Manifest removed the tick entirely and lets an order name any price in minor units. Its own README states the trade: without a tick, the newest order can stand in front of the queue by improving the price by a negligible amount, which inverts time priority.[^manifest-tickless]
@@ -44,13 +44,13 @@ The **market** account holds one trading pair's configuration: the two mint addr
The **order book** is the largest account: about 180 KB holding two trees of resting orders (one per side, 512 orders each) plus a monotonic `next_order_id` counter. Each tree has 1,024 nodes, and every order after the first takes two of them: a leaf for the order and a branch point where its key splits from the others. It is **zero-copy**: an account this large would be prohibitively expensive to deserialize on every instruction, so the program reads and writes fields in place through an `AccountLoader` instead.
-> **Why the order book is not at a PDA.** Solana caps account allocations made from inside a program at 10 KB, so `initialize_market()` cannot create a 180 KB account. Instead, the client creates the account directly with the System Program before calling `initialize_market()`, sized to `ORDER_BOOK_ACCOUNT_SIZE` and owned by the program. And its address cannot be a PDA at all, because creating an account that way requires the new account's own signature, which a PDA cannot give. The program instead ties the book to its market with a stored-address check: the market records the book's address, and every instruction that touches the book checks it against that record. So the book's address is a public key, and the figures draw its dot on the curve, with the book's box above it at the top right. Its private key signs once, at creation; after that the program owns the account and the key can do nothing to it.
+> **Why the order book is not at a PDA.** Solana caps account allocations made from inside a program at 10 KB, so `initialize_market()` cannot create a 180 KB account. Instead, the client creates the account directly with the System Program before calling `initialize_market()`, sized to `ORDER_BOOK_ACCOUNT_SIZE` and owned by the program. And its address cannot be a PDA at all, because creating an account that way requires the new account's own signature, which a PDA cannot give. The program instead ties the book to its market with a stored-address check: the market records the book's address, and every instruction that touches the book checks it against that record. So the book's address is a public key, and the figures draw its dot on the curve, with the book's box above it at the top right. Its private key signs once, at creation; after that the program owns the account and the private key can do nothing to it.
-Three **vaults** hold the money: a base vault for locked NVDAx, a quote vault for locked USDC, and a separate **fee vault** for Maria's earnings. Unlike the book, all three live at PDAs derived from the market (seeds: `"base_vault"`, `"quote_vault"` or `"fee_vault"`, then the market), and `initialize_market()` creates them with the market PDA as their authority. The fee vault holds the same token as the quote vault, and keeping them separate means balances owed to users and balances earned by the market can never be confused. That separation is what stops a caller from being paid out of Maria's fees; the failure modes section shows the attack it blocks.
+Three **vaults** hold the tokens: a base vault for NVDAx, a quote vault for USDC, and a separate **fee vault** for Maria's earnings. Unlike the book, all three live at PDAs derived from the market (seeds: `"base_vault"`, `"quote_vault"` or `"fee_vault"`, then the market), and `initialize_market()` creates them with the market PDA as their authority. The fee vault holds the same token as the quote vault, and keeping them separate means balances owed to users and balances earned by the market can never be confused. That separation is what stops a caller from being paid out of Maria's fees; the failure modes section shows the attack it blocks.
## Every Trader Gets a Ledger Line
-Three traders use the market this chapter: Alice, Bob, and Carol, each starting with the standard wallet of one SOL and 1,000 USDC, plus (for the two sellers, Alice and Carol) some NVDAx. Before trading, each of them calls `initialize_market_user()` once. It creates their market user account with zeroed unsettled balances and an empty open-order list. It is their line in the exchange's internal ledger: the account the exchange will owe money to.
+Three traders use the market this chapter: Alice, Bob, and Carol, each starting with the standard wallet of one SOL and 1,000 USDC, plus (for the two sellers, Alice and Carol) some NVDAx. Before trading, each of them calls `initialize_market_user()` once. It creates their market user account with zeroed unsettled balances and an empty open-order list. It is their line in the exchange's internal ledger: the account the exchange will owe NVDAx and USDC to.
That **market user** account (seeds: `"market_user"`, the market, the owner) tracks their `open_orders` (capped at 20) and two numbers that keep the design cheap: `unsettled_base` and `unsettled_quote`, the amounts the program owes them. When the program takes tokens from a user it transfers them immediately into a vault; when it owes tokens to a user it just increments these counters. Users claim what they are owed whenever they like, and many small payouts consolidate into one transfer, saving compute units on the path every fill takes.
@@ -58,7 +58,7 @@ That **market user** account (seeds: `"market_user"`, the market, the owner) tra
Alice holds NVDAx and thinks AI is overhyped. She wants out, but not at any price: she calls `place_order()` with side `Ask`, price 265, quantity ten shares.
-The handler validates first: the market is active, the price is nonzero and tick-aligned, the quantity meets the minimum, and Alice is under her 20-open-order cap. Then it takes her money up front. An ask locks the base: her ten shares move immediately from her token account into the base vault. (A bid locks the quote instead: price times quantity.) This lock-on-entry rule is what makes every order on the book a funded commitment rather than a promise; there is no path where a matched order turns out to be backed by nothing.
+The handler validates first: the market is active, the price is nonzero and tick-aligned, the quantity meets the minimum, and Alice is under her 20-open-order cap. Then it takes her tokens up front. An ask pays in the base token: her ten shares move immediately from her token account into the base vault. (A bid pays in the quote token instead: price times quantity.) Because the tokens move before the order rests, every order on the book is backed by tokens in a vault; there is no path where a matched order turns out to be backed by nothing.
The book's ask side is empty and no bid reaches 265, so there is nothing to match. Alice's order becomes a **resting order**: a leaf in the ask tree, an order account stamped Open, and an entry in her open-order list. Placing it cost her nothing but rent and the transaction fee; the exchange's own fee only ever applies to fills.
@@ -68,7 +68,7 @@ Every order placed becomes an **order** account of its own (seeds: `"order"`, th
## A Bid Below the Best Ask Rests Too
-Bob wants in, but he is not prepared to pay more than 250. He calls `place_order()` with side `Bid`, price 250, quantity four. The handler locks 4 × 250 = 1,000 USDC from his token account into the quote vault: every dollar he has, committed the moment the order rests. The best ask is Alice's 265, and Bob's limit refuses to cross it, so his bid rests too. The book now has a spread: bid 250, ask 265.
+Bob wants in, but he is not prepared to pay more than 250. He calls `place_order()` with side `Bid`, price 250, quantity four. The handler moves 4 × 250 = 1,000 USDC from his token account into the quote vault: every dollar he has, committed the moment the order rests. The best ask is Alice's 265, and Bob's limit refuses to cross it, so his bid rests too. The book now has a spread: bid 250, ask 265.

@@ -82,15 +82,15 @@ The fill executes at **the resting order's posted price**: 250, not Carol's 245.
One Solana-specific mechanic makes this work: the program cannot load arbitrary accounts mid-instruction, so Carol's transaction must carry the accounts of the makers she will match with, passed as pairs of (order account, market user account) in the order the book would walk them. The program does not trust that list. It plans fills from its own tree, then verifies each supplied pair against the plan: the order IDs must match, the accounts must belong to this market, and each maker order's owner must match its market user. A wrong or missing account fails the transaction with `MakerAccountMismatch`, `MakerOwnerMismatch`, or `MissingMakerAccounts`.
-Now the money moves, in the unsettled-balance style. Gross proceeds are 3 × 250 = 750 USDC. Maria's fee is 25 basis points of that: 1.875 USDC, computed with ceiling division so any rounding goes to the market rather than leaking value one minor unit per fill (the fee section below returns to this). Carol's `unsettled_quote` is credited with the net 748.125 USDC. Bob's `unsettled_base` is credited with three shares. The fee moves from the quote vault to the fee vault in a single program-signed transfer at the end, aggregated across all fills in the order. Bob's order account is stamped PartiallyFilled with three of four filled, and the leaf in the bid tree has its remaining quantity reduced to one in place. Carol's order account is stamped Filled; nothing of hers rests.
+Now the balances change, in the unsettled-balance style. Gross proceeds are 3 × 250 = 750 USDC. Maria's fee is 25 basis points of that: 1.875 USDC, computed with ceiling division so any rounding goes to the market rather than leaking value one minor unit per fill (the fee section below returns to this). Carol's `unsettled_quote` is credited with the net 748.125 USDC. Bob's `unsettled_base` is credited with three shares. The fee moves from the quote vault to the fee vault in a single program-signed transfer at the end, aggregated across all fills in the order. Bob's order account is stamped PartiallyFilled with three of four filled, and the leaf in the bid tree has its remaining quantity reduced to one in place. Carol's order account is stamped Filled; nothing of hers rests.

Bob did nothing at all. Like Alice in the *Escrow* chapter, a maker signs once when placing and the program handles their side of every subsequent fill.
-## Canceling Returns the Unfilled Lock
+## Canceling Returns the Unfilled Order's Tokens
-Bob decides three shares is enough. He calls `cancel_order()` on his order account. The handler checks the signer is the order's owner, and that the status is still Open or PartiallyFilled; a Filled or already-Cancelled order rejects with `OrderNotCancellable`. His remaining share at 250 means 250 USDC of his original lock is still sitting in the quote vault, so his `unsettled_quote` is credited 250, using the same price-times-remaining formula that locked it. The leaf comes out of the bid tree, the order ID leaves his open-order list, and the order account is stamped Cancelled, closing out its state machine.
+Bob decides three shares is enough. He calls `cancel_order()` on his order account. The handler checks the signer is the order's owner, and that the status is still Open or PartiallyFilled; a Filled or already-Cancelled order rejects with `OrderNotCancellable`. His remaining share at 250 means 250 USDC of what he paid in when he placed the order is still sitting in the quote vault, so his `unsettled_quote` is credited 250, using the same price-times-remaining formula that set how much he paid in. The leaf comes out of the bid tree, the order ID leaves his open-order list, and the order account is stamped Cancelled, closing out its state machine.

@@ -108,7 +108,7 @@ One line in the handler belongs in everything you build: it snapshots the unsett
## The Market Authority Withdraws the Fees
-At any time, Maria calls `withdraw_fees()`, and the fee vault's balance (1.875 USDC so far) transfers to her token account. The handler compares the signer against the authority recorded on the market at creation and rejects anyone else with `NotMarketAuthority`. When the fee vault is empty, the handler succeeds as a no-op instead of failing, so Maria can run it on a schedule unattended. Scheduled maintenance that succeeds as a no-op when there is nothing to do is a **crank** (the *Lending and Borrowing* chapter explains it fully), though this handler adds an authority gate because it moves fee money.
+At any time, Maria calls `withdraw_fees()`, and the fee vault's balance (1.875 USDC so far) transfers to her token account. The handler compares the signer against the authority recorded on the market at creation and rejects anyone else with `NotMarketAuthority`. When the fee vault is empty, the handler succeeds as a no-op instead of failing, so Maria can run it on a schedule unattended. Scheduled maintenance that succeeds as a no-op when there is nothing to do is a **crank** (the *Lending and Borrowing* chapter explains it fully), though this handler also checks the signer, because it moves the market's fees.

@@ -116,36 +116,36 @@ At any time, Maria calls `withdraw_fees()`, and the fee vault's balance (1.875 U
**Lots.** The program never stores "shares" or "dollars"; both sides of the book are denominated in **lots**. The market defines `base_lot_size` and `quote_lot_size`, and the two conversion rules are: raw base = quantity × base lot size, and raw quote = price × quantity × quote lot size. Choosing the lot sizes from the two tokens' decimals (each is ten to the power of the decimal difference, floored at one) makes `price` read as the human quote-per-base rate regardless of how mismatched the mints' decimals are. For NVDAx (8 decimals) over USDC (6 decimals) that gives a base lot of 100 raw NVDAx and a quote lot of 1, and a stored price of 250 reads directly as $250.00 per share. OpenBook and Phoenix both use a lot model like this. Manifest instead stores every quantity in minor units and widens the arithmetic, as this program already does for its intermediates.
-**Wider intermediates.** The bid lock and the gross fill amount both multiply three u64 values (price × quantity × lot size), which can overflow 64 bits with high-decimal mints. The program promotes to u128 for the multiplication and narrows back down afterward, so the failure mode is an explicit error on an amount too large to transfer, never wrong arithmetic.
+**Wider intermediates.** The USDC a bid pays into the vault and the gross fill amount both multiply three u64 values (price × quantity × lot size), which can overflow 64 bits with high-decimal mints. The program promotes to u128 for the multiplication and narrows back down afterward, so the failure mode is an explicit error on an amount too large to transfer, never wrong arithmetic.
-**The fee rounds up.** Fee = gross × fee basis points ÷ 10,000, computed with ceiling division. Rounding down would hand up to one minor unit of quote back per fill, and one minor unit per fill is not small when an attacker can manufacture thousands of tiny fills; rounding against the user, in the program's favor, closes the leak. A defensive check that the fee never exceeds the gross backs the arithmetic up. The rounding section of the *Arithmetic of Money* chapter states the general rule.
+**The fee rounds up.** Fee = gross × fee basis points ÷ 10,000, computed with ceiling division. Rounding down would hand up to one minor unit of quote back per fill, and one minor unit per fill is not small when an attacker can manufacture thousands of tiny fills; rounding against the user, in the program's favor, closes the leak. A defensive check that the fee never exceeds the gross backs the arithmetic up. The rounding section of the *Arithmetic of Money* chapter covers choosing a rounding direction in general.
-**Price improvement is computed, not assumed.** When a taker bid locks at its own limit but fills at a better resting price, the difference between the lock and the gross is credited back to the taker's unsettled quote, calculated per fill with the same u128 care.
+**Price improvement is computed, not assumed.** When a taker's bid pays into the vault at its own limit price but fills at a better resting price, the difference between what it paid in and the gross is credited back to the taker's unsettled quote. The difference is calculated per fill with the same u128 care.
## The Invariants: The Vaults Equal What the Book Owes
-- Every resting order's full lock is in a vault: quote for bids (price × remaining × quote lot size), base for asks. The vaults always hold at least the sum of all resting locks plus all unsettled balances.
-- Every locked minor unit is accounted for on exit: a fill splits the lock into maker proceeds, taker rebate, and fee, and those three always sum exactly to what was locked for that fill. Cancellation and eviction each return exactly the remainder's lock.
+- For every order still on the book, the vaults hold everything that order would pay if the rest of it filled: the USDC for a bid (price × remaining quantity × quote lot size) and the NVDAx for an ask. The vaults always hold at least those amounts for every resting order, plus all unsettled balances.
+- Every minor unit an order paid into a vault is accounted for when it leaves: a fill splits what was held for it into maker proceeds, taker rebate, and fee, and those three always sum exactly to what was held for that fill. Cancellation and eviction each return exactly what was held for the unfilled remainder.
- `filled_quantity` never exceeds `original_quantity`, and order status only moves forward: Open to PartiallyFilled to Filled, or out to Cancelled.
- The fee vault only ever receives the computed fee, and only the recorded authority can withdraw from it.
- Every leaf in a tree corresponds to exactly one Open or PartiallyFilled order account, and to an entry in its owner's open-order list.
## The Failure Modes: Vault Swaps, Fake Makers, and Book Spam
-- **The vault swap.** The fee vault and the quote vault hold the same token and have the same authority, so a token-program transfer cannot tell them apart. Without further checks, a caller invoking `settle_funds()` could pass the fee vault where the quote vault belongs and be paid out of Maria's fees. The defense is a stored-address check on every instruction, binding every vault and mint to the addresses recorded on the market at creation, and the test suite proves the attack fails. This is the same account-substitution attack the *Escrow* chapter described, against a bigger program.
+- **The vault swap.** The fee vault and the quote vault hold the same token and have the same authority, so a token-program transfer cannot tell them apart. Without further checks, a caller invoking `settle_funds()` could pass the fee vault where the quote vault belongs and be paid out of Maria's fees. The defense is a stored-address check on every instruction, binding every vault and mint to the addresses recorded on the market at creation, and `settle_funds_rejects_fee_vault_substituted_for_quote_vault` checks that the attack fails. This is the same account-substitution attack the *Escrow* chapter described, against a bigger program.
- **The fake maker list.** Takers supply the maker accounts they match against, so a hostile taker will supply the wrong ones. The program re-derives the fills from its own book and verifies every supplied account against that plan: order ID, market, and owner all have to line up, or the transaction fails.
- **The rounding drain.** Floor the fee instead of ceiling it and every fill leaks a minor unit, which an attacker turns into a drain with thousands of tiny fills. The math section covered it, and a test below checks it.
-- **Book spam.** Each side holds 512 orders, so a trader willing to lock the minimum order size and pay rent 512 times can fill a side with orders far from the spread. If a full side refused every new order, that trader could keep every new order off the side for as long as they liked. Maria could not move the pair either, because the market PDA's seeds are the two mints. The program stops this with **eviction**. When an order would rest on a full side, `place_order()` compares its price with that side's worst order, the lowest bid or the highest ask. A better price removes the worst order and takes its slot, and the evicted owner is refunded through their unsettled balance, exactly as `cancel_order()` refunds them. An order that is no better, including one at an equal price, where the resting order was there first, is rejected with `OrderBookFull`. The orders that go are the ones least likely ever to fill, so a book full of far-off orders stays open at the prices that trade. The placer passes the worst order and its owner's market user account after the maker pairs, and the program checks them against the book the way it checks maker accounts, rejecting anything else with `EvictedAccountMismatch`. Tick alignment, the minimum order size, and the 20-open-orders-per-user cap still apply, so every slot costs its occupant locked capital.
+- **Book spam.** Each side holds 512 orders, so a trader willing to pay the minimum order size into the vault and pay rent 512 times can fill a side with orders far from the spread. If a full side refused every new order, that trader could keep every new order off the side for as long as they liked. Maria could not move the pair either, because the market PDA's seeds are the two mints. The program stops this with **eviction**. When an order would rest on a full side, `place_order()` compares its price with that side's worst order, the lowest bid or the highest ask. A better price removes the worst order and takes its slot, and the evicted owner is refunded through their unsettled balance, exactly as `cancel_order()` refunds them. An order at the same price or worse is rejected with `OrderBookFull`, because the resting order was there first. Eviction only removes the order furthest from the market price, the one least likely to fill, so a spammer's far-off orders cannot keep new orders off the book. The placer passes the worst order and its owner's market user account after the maker pairs, and the program checks them against the book the way it checks maker accounts, rejecting anything else with `EvictedAccountMismatch`. Tick alignment, the minimum order size, and the 20-open-orders-per-user cap still apply, so every slot ties up its occupant's tokens in a vault.
- **The unauthorized paths.** Canceling someone else's order rejects with `Unauthorized`; withdrawing fees without being the authority rejects with `NotMarketAuthority`; placing orders on a paused market rejects with `MarketPaused`.
## Testing Considerations
The suite in `tests/test_order_book.rs` builds all state through the program's own instruction handlers, and covers each code path above by name:
-- Locking: `place_bid_locks_quote_in_vault`, `place_ask_locks_base_in_vault`, and the rejection tests for zero prices, unaligned ticks, and undersized orders.
+- Paying in when an order is placed: `place_bid_moves_quote_into_vault`, `place_ask_moves_base_into_vault`, and the rejection tests for zero prices, unaligned ticks, and undersized orders.
- Matching, both directions and both partial-fill shapes: `taker_bid_fully_crosses_best_ask`, `taker_ask_fully_crosses_best_bid`, `taker_partially_fills_resting_order_rest_stays_on_book`, `taker_partially_filled_remainder_rests_on_book`, and `taker_crosses_multiple_resting_orders_best_price_first`.
-- The fairness rules: `resting_orders_at_same_price_fill_by_time_priority` and `taker_bid_gets_price_improvement_from_resting_ask`.
-- The money: `fee_vault_receives_exactly_bps_of_taker_gross`, `fee_rounds_up_when_gross_is_not_a_bps_multiple`, `cancel_and_settle_bid_refunds_full_quote`, and `settle_funds_after_match_pays_out_both_unsettled_balances`.
+- Price-time priority and price improvement: `resting_orders_at_same_price_fill_by_time_priority` and `taker_bid_gets_price_improvement_from_resting_ask`.
+- Fees and payouts: `fee_vault_receives_exactly_bps_of_taker_gross`, `fee_rounds_up_when_gross_is_not_a_bps_multiple`, `cancel_and_settle_bid_refunds_full_quote`, and `settle_funds_after_match_pays_out_both_unsettled_balances`.
- The adversarial cases: `settle_funds_rejects_fee_vault_substituted_for_quote_vault`, `cancel_order_rejects_non_owner`, and `authority_can_withdraw_fees_after_match`.
- The tree's worst case: `doubling_prices_build_the_deepest_path_prices_allow` and `deepest_path_adds_little_compute_to_insert_fill_and_cancel`.
- Eviction, each test starting from a bid side filled with 512 orders: `full_side_refuses_an_order_no_better_than_its_worst`, `better_order_evicts_the_worst_and_rests`, `evicted_maker_settles_their_refund`, `eviction_rejects_missing_or_wrong_evicted_accounts`, and `trader_can_evict_their_own_worst_order`.
@@ -158,11 +158,11 @@ NYSE, NASDAQ, and the CME each run a matching engine, a clearing house, and a se
The order book added six concepts on top of the escrow's:
-- **Every resting order is a funded commitment.** Lock-on-entry means no matched order can turn out to be backed by nothing.
-- **A data structure chosen for onchain cost.** The critbit tree gives insertion without shifting and price-time priority for free, on a chain where compute is metered.
+- **Every resting order is a funded commitment.** `place_order()` moves the tokens into a vault before the order rests, so no matched order can turn out to be backed by nothing.
+- **A data structure chosen for onchain cost.** The critbit tree inserts without shifting other entries, and walking it visits orders in price-time priority, on a chain where compute is metered.
- **An internal ledger that consolidates payouts.** Unsettled balances turn many small payouts into one claimed transfer, and zeroing them before the transfer (checks-effects-interactions) makes double-withdrawal impossible.
- **A fee whose rounding direction is a security decision.** Ceiling division on the fee closes a leak an attacker could industrialize through thousands of tiny fills.
-- **Vault separation plus stored-address checks.** Two vaults holding the same token are indistinguishable to the token program; only stored-address checks keep user money and fee money apart.
+- **Vault separation plus stored-address checks.** Two vaults holding the same token are indistinguishable to the token program; only stored-address checks keep users' balances and the market's fees apart.
- **A full book evicts its worst order.** Account space is finite, so a side that refused every order once full would let anyone with the capital to fill it keep every new order off it. Eviction trades the order least likely to fill for a better one, refunding it as a cancel would.
In the *Constant-Product AMM* chapter we will build a market with no order book. Two vaults hold the pair of tokens, and a formula sets the price from their balances, so anyone can trade at any time without waiting for a counterparty to post an order. You will see what the people who fill the vaults earn for it, and what it costs them.
diff --git a/building-financial-products-in-solana/perpetual-futures.md b/building-financial-products-in-solana/perpetual-futures.md
index e378566f..9304ff83 100644
--- a/building-financial-products-in-solana/perpetual-futures.md
+++ b/building-financial-products-in-solana/perpetual-futures.md
@@ -6,15 +6,15 @@ The program lives at `finance/perpetual-futures` in the companion repository. It
## The Pool Is the Other Side of the Trade
-In the *Order Book Exchange* chapter, every long met a short: Carol's ask crossed Bob's bid, and the program merely kept score. A pool-based perp removes the matching problem entirely. **Liquidity providers fund one pool, and the pool is the counterparty to every position.** When a trader wins, the pool pays; when a trader loses, the pool keeps the loss. So an LP's return is fees and funding collected, minus whatever the winning traders take.
+In the *Order Book Exchange* chapter, every long met a short: Carol's ask crossed Bob's bid, and the program merely kept score. A perp that trades against a pool has no orders to match. **Liquidity providers fund one pool, and the pool is the counterparty to every position.** When a trader wins, the pool pays; when a trader loses, the pool keeps the loss. So an LP's return is the losing traders' losses plus the funding the pool collects, minus whatever the winning traders take. The open and close fees go to the pool's creator, not to the LPs.
-That design decision creates the problem the program is organized around: the pool must be able to prove it can pay every winner, at every moment, **without ever iterating over the open positions**. A program that must touch every position to answer "am I solvent?" stops fitting in a transaction the day it succeeds. That requirement explains almost every field in the account model.
+That design decision creates the problem the program is organized around: the program must be able to check that the pool can pay every winner, at every moment, **without ever iterating over the open positions**. A program that must touch every position to answer "am I solvent?" stops fitting in a transaction once it has enough open positions. That requirement explains almost every field in the account model.

## Creating the Market, Where the Parameters Are Checked Against Each Other
-Maria creates the pool and is its `authority`; her revenue is the open and close fees that accrue to `protocol_fees`, and the program gives her no path to anyone's collateral. She calls `initialize_pool()` with the market's risk parameters: 10 basis-point open and close fees, 10x maximum leverage, a 5% maintenance margin, a 1% liquidation fee, and a 1% cap on oracle confidence. The handler validates each parameter against `BASIS_POINTS_DENOMINATOR`, and one cross-check prevents a dead state: `maintenance_margin_bps` must exceed `close_fee_bps`. Otherwise a position could exist that is too healthy to liquidate but too poor to pay its own close fee, leaving its owner with no exit at all. The test `test_initialize_pool_rejects_close_fee_at_or_above_maintenance_margin` checks it. Every state needs an exit, and here that principle reaches back into the *configuration* of the market.
+Maria creates the pool and is its `authority`; her revenue is the open and close fees that accrue to `program_fees`, and the program gives her no path to anyone's collateral. She calls `initialize_pool()` with the market's risk parameters: 10 basis-point open and close fees, 10x maximum leverage, a 5% maintenance margin, a 1% liquidation fee, and a 1% cap on oracle confidence. The handler validates each parameter against `BASIS_POINTS_DENOMINATOR`, and one cross-check prevents a dead state: `maintenance_margin_bps` must exceed `close_fee_bps`. Otherwise a position could exist that is too healthy to liquidate but too poor to pay its own close fee, and neither `close_position()` nor `liquidate_position()` would accept it, leaving its owner no way to get their collateral out. The test `test_initialize_pool_rejects_close_fee_at_or_above_maintenance_margin` checks it. So the requirement that every participant must be able to get their assets out of every state is enforced in the market's *configuration*, before any position exists.

@@ -24,22 +24,22 @@ The **pool** account lives at a PDA with seeds `"pool"`, the collateral mint, an
- `liquidity`: what the LPs own. It grows with deposits, trader losses, and funding; it shrinks with withdrawals and trader profits.
- `reserved_liquidity`: the portion of `liquidity` currently pledged as the maximum payout of open positions.
-- `total_collateral`: trader money held for open positions. It is tracked separately from `liquidity` because it is not the LPs' money.
-- `protocol_fees`: Maria's accrued fees, separate again.
+- `total_collateral`: traders' collateral held for open positions. It is tracked separately from `liquidity` because it belongs to the traders, not the LPs.
+- `program_fees`: Maria's accrued fees, separate again.
- `long_size`, `short_size`, `long_size_scaled`, `short_size_scaled`: per-side open-interest accumulators, which make solvency checkable without iteration.
- `cumulative_funding` and `last_funding_timestamp`: the funding index.
-The pool is the mint authority of the **LP share mint** (seeds `"lp_mint"`) and the token authority of the **custody vault** (seeds `"vault"`), a single token account holding LP liquidity, trader collateral, and fees together, and signs for both with its own seeds. The vault mixes custody; the three ledger fields keep the ownership straight, and the rule that makes that safe is the *Fundraiser* chapter's: balances are custody, state is truth.
+The pool is the mint authority of the **LP share mint** (seeds `"lp_mint"`) and the token authority of the **custody vault** (seeds `"vault"`), a single token account holding LP liquidity, trader collateral, and fees together, and signs for both with its own seeds. The vault holds everyone's tokens together, and the three ledger fields, `liquidity`, `total_collateral`, and `program_fees`, record how much of it belongs to whom. As in the *Fundraiser* chapter, every handler computes amounts from those fields, never from the vault balance.
## Liquidity Providers Fund the Pool and Get Shares Priced at Mark
-Carol is a liquidity provider. Her income is every losing trader's loss and every funding payment; her risk is every winning trader's profit. She calls `add_liquidity()` with her whole 1,000 USDC. She is the first depositor, so her shares are her deposit minus `MINIMUM_LIQUIDITY`: a withheld 1,000 minor units, a tenth of a cent, that no one will ever own. That is the same first-depositor defense the *Constant-Product AMM* chapter built. Both `add_liquidity()` and `remove_liquidity()` count the withheld shares in the supply they divide by, so their slice of the pool stays locked for good, and the failure modes below show the attack that lock stops. The *Managed Fund* chapter ignores donations instead. Later deposits mint shares pro rata against **assets under management marked to market**, rather than against the vault balance or `liquidity` alone: `liquidity` minus the traders' current unrealized profit (or plus their unrealized loss). If LP shares were priced while ignoring open positions, a depositor could buy in cheap the moment traders were winning and dilute Carol at her worst moment. Marking AUM makes entering and leaving the pool fair at any instant, and both `add_liquidity()` and `remove_liquidity()` take a minimum-out argument so a price move between signing and execution fails with `SlippageExceeded` instead of filling badly. Eleven more providers follow Carol in over the morning, and the pool opens for trading with 12,000 USDC of liquidity.
+Carol is a liquidity provider. Her income is every losing trader's loss and every funding payment; her risk is every winning trader's profit. She calls `add_liquidity()` with her whole 1,000 USDC. She is the first depositor, so her shares are her deposit minus `MINIMUM_LIQUIDITY`: a withheld 1,000 minor units, a tenth of a cent, that no one will ever own. That is the same first-depositor defense the *Constant-Product AMM* chapter built. Both `add_liquidity()` and `remove_liquidity()` count the withheld shares in the supply they divide by, so their slice of the pool stays locked for good, and the failure modes below show the attack those withheld shares stop. The *Managed Fund* chapter ignores donations instead. Later deposits mint shares pro rata against **assets under management marked to market**, rather than against the vault balance or `liquidity` alone: `liquidity` minus the traders' current unrealized profit (or plus their unrealized loss). If LP shares were priced while ignoring open positions, a depositor could buy in cheap the moment traders were winning and dilute Carol at her worst moment. Marking AUM makes entering and leaving the pool fair at any instant, and both `add_liquidity()` and `remove_liquidity()` take a minimum-out argument so a price move between signing and execution fails with `SlippageExceeded` instead of filling badly. Eleven more providers follow Carol in over the morning, and the pool opens for trading with 12,000 USDC of liquidity.
-Withdrawals have one extra gate: only **free** liquidity, `liquidity − reserved_liquidity`, may leave (`InsufficientLiquidity` otherwise, checked by `test_remove_liquidity_blocked_by_reserved`). Carol can never pull out money the pool has promised to a winner.
+`remove_liquidity()` makes one extra check: only **free** liquidity, `liquidity − reserved_liquidity`, may leave, and a larger withdrawal fails with `InsufficientLiquidity` (checked by `test_remove_liquidity_blocked_by_reserved`). Carol can never withdraw liquidity the pool has reserved for a winner.
## The Price Comes From Outside, So Every Handler Checks It Before Using It
-Every handler that touches money begins the same way: read the oracle, validate it, accrue funding. The pool records one **oracle feed** account at creation, the mechanism from the *Offchain Truth* chapter, at production scale. The feed carries a price written onchain by a network whose signatures prove its origin, read with the same five checks the *Prop AMM* chapter ran. In the companion repository the feed is a mock, an authority-gated stand-in the tests can set prices on, labeled not-for-production; the production path is a real feed network's account, decoded and verified with that network's own crate. Either way the program runs five checks on what it reads, in order:
+Every trading and liquidity handler begins the same way: read the oracle, validate it, accrue funding. The pool records one **oracle feed** account at creation, the mechanism from the *Offchain Truth* chapter, at production scale. The feed carries a price written onchain and signed by the feed network that published it, read with the same five checks the *Prop AMM* chapter ran. In the companion repository the feed is `mock-price-feed`, a program labeled not-for-production whose authority sets the price with `set_price()`, so the tests can move it; the production path is a real feed network's account, decoded and verified with that network's own crate. Either way the program runs five checks on what it reads, in order:
- the account is long enough to contain a price (`OracleDataTooShort`),
- the price is positive (`NonPositivePrice`),
@@ -51,13 +51,13 @@ A stale price here invites arbitrage. Anyone who can trade a leveraged market at
## Opening a Long, and the Pool Reserves the Best Case
-Alice thinks the quarter goes NVIDIA's way and goes long; Bob believes the opposite and will open the mirror-image short. Alice calls `open_position()`: side Long, 1,000 USDC collateral, 5,000 USDC size, and an acceptable-price bound (her slippage guard; a long opening above it fails with `SlippageExceeded`). Size five times her collateral means **5x leverage**: every 1% move in NVDAx moves her equity by about 5%. The handler charges the open fee on **notional size, not collateral**: 10 basis points of 5,000 is 5 USDC, so her working collateral is 995. Fees scale with exposure because exposure, not deposit, is what the pool underwrites. Then the handler makes three checks, in order, and refuses the position if any fails:
+Alice thinks the quarter goes NVIDIA's way and goes long; Bob believes the opposite and will open the mirror-image short. Alice calls `open_position()`: side Long, 1,000 USDC collateral, 5,000 USDC size, and an acceptable-price bound (her slippage check; a long opening above it fails with `SlippageExceeded`). Size five times her collateral means **5x leverage**: every 1% move in NVDAx moves her equity by about 5%. The handler charges the open fee on **notional size, not collateral**: 10 basis points of 5,000 is 5 USDC, so her working collateral is 995. Fees scale with exposure because exposure, not deposit, is what the pool underwrites. Then the handler makes three checks, in order, and refuses the position if any fails:
- **Leverage**: size must not exceed net collateral times `max_leverage`. 5,000 against 995 × 10 = 9,950 passes; her effective leverage is about 5x. Eleven-x fails with `LeverageTooHigh` (checked by `test_open_rejects_excess_leverage`).
- **Born healthy**: net collateral must exceed the maintenance margin on her size, 5% of 5,000 = 250 USDC, else `PositionNotHealthy`. A position must not be liquidatable in the transaction that creates it.
- **The reservation**: `reserved_liquidity` grows by her full size, 5,000, and the sum must stay within `liquidity`, else `InsufficientLiquidity` (checked by `test_open_rejects_when_pool_cannot_back_it`).
-By reserving the notional, the pool sets aside the most it could ever owe her, because **profit on a close is capped at the position's size**. The cap has a cost for Alice: if NVDAx tripled to $300, her raw profit of 10,000 USDC would be paid as 5,000. In exchange, every winner's payout is pre-funded before the position exists, so the pool can never owe more than it holds. The test `test_profit_capped_at_reserved_notional` runs the tripling and asserts the cap. Production venues layer insurance funds and auto-deleveraging above this floor; the floor is the part that must never be missing.
+By reserving the notional, the pool sets aside the most it could ever owe her, because **profit on a close is capped at the position's size**. The cap has a cost for Alice: if NVDAx tripled to $300, her raw profit of 10,000 USDC would be paid as 5,000. In exchange, every winner's payout is pre-funded before the position exists, so the pool can never owe more than it holds. The test `test_profit_capped_at_reserved_notional` runs the tripling and asserts the cap. Production venues add insurance funds and auto-deleveraging on top of this reservation.
> **Where a loss too big to pay goes.** A leveraged position can lose more than the collateral standing behind it, and somebody still has to pay the winner on the other side. Venues answer with a waterfall, where each tier takes the shortfall from someone different:
>
@@ -65,9 +65,9 @@ By reserving the notional, the pool sets aside the most it could ever owe her, b
> - **An insurance fund** takes it from a pot the venue filled out of fees, so that what the collateral could not cover does not come from the LPs.
> - **Auto-deleveraging** takes it from the winners. The venue closes the most profitable leveraged positions on the winning side at the mark, pays them out early, and retires exposure nobody can fund.
>
-> This program stops at the first tier and caps profit at the reservation, so the shortfall can never open in the first place. An insurance fund and auto-deleveraging do not make a venue lose less money. They name, before the loss happens, which users pay for it.
+> This program stops at the first tier and caps profit at the reservation, so the shortfall can never open in the first place. An insurance fund and auto-deleveraging do not make a venue's losses smaller. They name, before the loss happens, which users pay for it.
-Her transaction ends with the only token movement: 1,000 USDC from Alice's token account into the vault. Bob opens his mirror-image short the same way: 1,000 collateral, 5,000 size, entry $100, another 5 USDC fee to `protocol_fees`, another 5,000 reserved.
+Her transaction ends with the only token movement: 1,000 USDC from Alice's token account into the vault. Bob opens his mirror-image short the same way: 1,000 collateral, 5,000 size, entry $100, another 5 USDC fee to `program_fees`, another 5,000 reserved.

@@ -90,62 +90,59 @@ The rate is quoted per second of wall-clock time, and the index stores the Clock
## NVDAx Rallies: The Short Loses Value, and a Liquidator Is Paid to End It
-The quarter goes Alice's way: NVDAx climbs to $110. Mark both positions to market with the program's PnL formula: size × price change ÷ entry price. Alice is up 500. Bob is down 500, and Bob's **equity**, working collateral plus PnL minus funding owed, is 995 − 500 = 495 USDC. His maintenance margin is 250, so he is safe, and `test_healthy_position_cannot_be_liquidated` guarantees Dave cannot touch a healthy position (`PositionHealthy`).
+The quarter goes Alice's way: NVDAx climbs to $110. Mark both positions to market with the program's PnL formula: size × price change ÷ entry price. Alice is up 500. Bob is down 500, and Bob's **equity**, working collateral plus PnL minus funding owed, is 995 − 500 = 495 USDC. His maintenance margin is 250, so he is safe: `liquidate_position()` refuses a healthy position with `PositionHealthy`, which `test_healthy_position_cannot_be_liquidated` checks.
-At $115, Bob's equity is 995 − 750 = 245, under the 250 maintenance line. He should close, but that means admitting he was wrong, and he keeps putting it off. Dave watches liquidations: he runs a bot that calls `liquidate_position()` on any account whose equity has fallen to or below maintenance margin, and he does it for the liquidation fee, not for the good of the market. Anyone may call `liquidate_position()`, and his bot does; the health check is the only permission that matters. The payout waterfall, every branch of which the program walks in order:
+At $115, Bob's equity is 995 − 750 = 245, under the 250 maintenance line. He should close, but that means admitting he was wrong, and he keeps putting it off. Dave watches liquidations: he runs a bot that calls `liquidate_position()` on any account whose equity has fallen to or below maintenance margin, and he does it for the liquidation fee, not for the good of the market. Anyone may call `liquidate_position()`, and his bot does: the handler checks the position's health, not who is calling. The payout waterfall, every branch of which the program walks in order:
- Bob's remaining equity, floored at zero: 245 USDC.
- Dave takes the liquidation fee, 1% of the 5,000 notional = 50 USDC, capped at remaining equity: 50.
- Bob is refunded the rest: 195 USDC, plus the rent from his position account, which closes.
- The LPs absorb the difference between Bob's collateral and what walked out: 995 − 245 = 750 USDC to `liquidity`.
-The pool cannot sell Bob's position; a program never acts on its own, somebody has to call it, so the design pays outsiders to call it at the right moment. Dave is the crank pattern from the *Lending and Borrowing* chapter with a profit motive. The example documents one gap: the liquidation fee comes out of *remaining equity*, so a position that gaps straight through zero pays Dave nothing, and positions nobody liquidates become losses the LPs absorb. Production venues fund the liquidator from an insurance fund so the incentive never disappears. When your design relies on someone showing up, check what happens when showing up stops paying.
+The pool cannot sell Bob's position; a program never acts on its own, somebody has to call it, so the design pays outsiders to call it at the right moment. Dave is the crank pattern from the *Lending and Borrowing* chapter with a profit motive. The example documents one gap: the liquidation fee comes out of *remaining equity*, so a position that gaps straight through zero pays Dave nothing, and positions nobody liquidates become losses the LPs absorb. Production venues fund the liquidator from an insurance fund so the incentive never disappears.

## Closing at $120, and the Pool Pays From Its Reservation
-At $120 Alice takes her win. `close_position()` marks her position one last time: profit 5,000 × 20 ÷ 100 = 1,000 USDC, under her 5,000 cap; close fee 10 basis points of size = 5; payout 995 + 1,000 − 5 = **1,990 USDC**. That nearly doubles her stake on a 20% move, which is what 5x leverage means. Her 5,000 reservation releases, the LPs' `liquidity` drops by her 1,000 profit, `protocol_fees` collects the 5, her position account closes, and the vault pays her out under the pool's signature. The same handler protects the pool from the other direction. A position whose equity cannot cover its own close fee is refused with `PositionNotHealthy` and must exit through liquidation, and any payout the pool arithmetic cannot cover is refused with `PoolInsolvent` rather than overdrawing the LPs.
+At $120 Alice takes her win. `close_position()` marks her position one last time: profit 5,000 × 20 ÷ 100 = 1,000 USDC, under her 5,000 cap; close fee 10 basis points of size = 5; payout 995 + 1,000 − 5 = **1,990 USDC**. That nearly doubles her stake on a 20% move, which is what 5x leverage means. Her 5,000 reservation releases, the LPs' `liquidity` drops by her 1,000 profit, `program_fees` collects the 5, her position account closes, and the vault pays her out under the pool's signature. The same handler protects the pool from the other direction. A position whose equity cannot cover its own close fee is refused with `PositionNotHealthy` and can only be closed by `liquidate_position()`, and any payout the pool arithmetic cannot cover is refused with `PoolInsolvent` rather than overdrawing the LPs.
-Everyone ends the story with what their position promised:
+Here is where everyone ends up:
- **Alice** turned 1,000 into 1,990 by being right with leverage.
- **Bob** turned 1,000 into 195 by being wrong and slow.
- **Dave** collected 50 USDC for one well-timed transaction.
-- **Maria**'s `protocol_fees` holds 15, from two opens and one close (Bob's liquidation charges no close fee). She sweeps it with `collect_fees()`, the only handler gated on her signature, and only to zero the fee ledger (`test_collect_fees_requires_authority` checks the gate).
-- **Carol** owns a twelfth of the pool, which absorbed Bob's 750 loss and paid Alice's 1,000 profit: down 250 on the trading, about 21 dollars of it hers, plus fees and funding on the next thousand traders.
-
-The pool does not need every trade to go its way, only most of them over time.
-
+- **Maria**'s `program_fees` holds 15, from two opens and one close (Bob's liquidation charges no close fee). She sweeps it with `collect_fees()`, which pays out what the fee ledger holds and sets it to zero. That handler and `set_funding_rate()` both refuse any signer but her (`test_collect_fees_requires_authority`, `test_only_authority_can_set_funding_rate`).
+- **Carol** owns a twelfth of the pool, which absorbed Bob's 750 loss and paid Alice's 1,000 profit: down 250 on the trading, about 21 dollars of it hers, plus the losses and funding of the next thousand traders.
## The Math: One Formula per Concept, All Floored in the Pool's Favor
Every calculation is checked u128 integer arithmetic, multiply before divide, and every division rounds in the pool's favor: `basis_points_of()` floors fees and margin thresholds, share minting and withdrawal amounts floor against AUM, and PnL truncates toward zero. Prices arrive at the oracle's fixed-point scale (10^8 here) and cancel out of the PnL formula, so positions settle in collateral minor units. The per-side scaled accumulator is what keeps the cost constant: each open adds size ÷ entry price (at `SIZE_PRECISION`) to its side. The pool's whole unrealized PnL is then two multiplications against the current price, so AUM, and with it LP share pricing and solvency, computes in constant time forever.
## The Invariants: Percolator's Three, Simplified
-The example's design notes point at Percolator, Anatoly Yakovenko's formally verified perp risk engine, and the three invariants it enforces are the ones to learn, because this program is a simplified working model of each:
+The example's design notes point at Percolator, Anatoly Yakovenko's Kani-checked perp risk engine, and the three invariants it enforces are the ones to learn, because this program is a simplified working model of each:
-- **Realizable credit**: every payout the system may ever owe is backed before it can be owed. Here: reservations at open, the profit cap, the free-liquidity gate on withdrawals, and the `PoolInsolvent` checks that refuse a payout the pool cannot cover.
+- **Realizable credit**: every payout the system may ever owe is backed before it can be owed. Here: reservations at open, the profit cap, the free-liquidity check in `remove_liquidity()`, and the `PoolInsolvent` checks that refuse a payout the pool cannot cover.
- **Account-local safety**: any position can be settled by touching that position and the pool, never a third account. Here: the entry-snapshot fields on `Position`.
- **Bounded progress**: no operation's cost grows with the number of open positions. Here: the per-side accumulators and one-at-a-time liquidation.
-The book's standing invariants hold here too. The vault balance always equals `liquidity + total_collateral + protocol_fees` plus dust; share value cannot be moved by donating tokens to the vault, because AUM reads tracked state, not balances; and every position born healthy has, at all times, at least one working exit.
+The book's standing invariants hold here too. The vault balance always equals `liquidity + total_collateral + program_fees` plus dust; share value cannot be moved by donating tokens to the vault, because AUM reads tracked state, not balances; and every position born healthy can, at all times, be closed by either `close_position()` or `liquidate_position()`.
## The Failure Modes: Leverage Amplifies Whatever You Got Wrong
-- **A stale or manipulated price** is the catastrophic one: with 10x leverage, a 1% price error is a 10% equity error, market-wide, simultaneously. That explains the five checks, the confidence cap, and the one trust decision the program leaves visible: the pool creator chooses the feed, and the feed account's owner is deliberately unchecked. Choose oracles the way you choose custodians.
+- **A stale or manipulated price** is the catastrophic one: with 10x leverage, a 1% price error is a 10% equity error, market-wide, simultaneously. That explains the five checks, the confidence cap, and the one trust decision the program leaves visible: the pool creator chooses the feed, and the feed account's owner is deliberately unchecked, so whoever controls that feed sets every price the pool settles at.
- **The unliquidated-position gap**: liquidations that pay nothing do not happen, and the LPs absorb the loss. The example documents this gap, and it is why production venues add an insurance fund.
-- **First-depositor share inflation**: a donation to the vault moves nothing, because AUM reads `liquidity`, not the vault balance. But `liquidity` grows with every funding payment and trader loss, and a liquidity provider can also be the pool's only trader. An attacker holding a single share can pay funding into the pool from their own position to make that share expensive, then wait for a deposit to round down. The withheld `MINIMUM_LIQUIDITY` makes this cost them, because it counts as 1,000 shares in both divisors: the attacker's share is 1 of 1,001, and almost everything they pay in stays with the locked shares. A deposit mints deposit × (shares outstanding + 1,000) ÷ AUM. In the test the attacker pumps AUM to just over 1,000 USDC and the victim deposits just under twice that, which mints the victim 2,001 shares against the attacker's 1. Dividing by the bare supply of 1 would have minted the victim a single share. `test_inflating_liquidity_through_own_trades_does_not_pay` runs the attack: the attacker pays about 1,000 USDC of funding and gets back about 1 USDC. Divided by the bare supply instead, the same attack returns them about 1,500 USDC, 500 of it from the victim's 2,000 USDC deposit. `test_first_deposit_below_minimum_fails` proves a dust first deposit cannot even open the pool.
+- **First-depositor share inflation**: a donation to the vault moves nothing, because AUM reads `liquidity`, not the vault balance. But `liquidity` grows with every funding payment and trader loss, and a liquidity provider can also be the pool's only trader. An attacker holding a single share can pay funding into the pool from their own position to make that share expensive, then wait for a deposit to round down. The withheld `MINIMUM_LIQUIDITY` makes this cost them, because it counts as 1,000 shares in both divisors: the attacker's share is 1 of 1,001, and almost everything they pay in stays with the locked shares. A deposit mints deposit × (shares outstanding + 1,000) ÷ AUM. In the test the attacker pumps AUM to just over 1,000 USDC and the victim deposits just under twice that, which mints the victim 2,001 shares against the attacker's 1. Dividing by the bare supply of 1 would have minted the victim a single share. `test_inflating_liquidity_through_own_trades_does_not_pay` runs the attack: the attacker pays about 1,000 USDC of funding and gets back about 1 USDC. Divided by the bare supply instead, the same attack returns them about 1,500 USDC, 500 of it from the victim's 2,000 USDC deposit. `test_first_deposit_below_minimum_fails` checks that a first deposit smaller than `MINIMUM_LIQUIDITY` is refused with `DepositTooSmall`.
- **Winning too big**: profit beyond the reservation is clipped by the cap rather than bankrupting the pool, and traders know the cap before they open.
-- **Sandwiching and price drift**: every money handler takes a user-supplied bound and fails with `SlippageExceeded` instead of filling at a surprise price.
-- **Double close, double liquidation**: the position account closes on exit; the second call finds no account.
+- **Sandwiching and price drift**: every trader and LP handler takes a user-supplied bound and fails with `SlippageExceeded` instead of filling at a surprise price.
+- **Double close, double liquidation**: the position account closes when the position is closed or liquidated; the second call finds no account.
- **Dangerous configurations**: parameter validation at pool creation, including the maintenance-versus-close-fee cross-check.
## Testing Considerations
The suite is the largest in the companion repository: a mock oracle the tests set prices on, and a `Market` helper that builds the pool, warps slots, and re-derives position PDAs. The pattern to copy is **one test per invariant, named as the sentence it checks**: `test_profit_capped_at_reserved_notional`, `test_remove_liquidity_blocked_by_reserved`, `test_healthy_position_cannot_be_liquidated`, `test_stale_price_rejected`, `test_collect_fees_requires_authority`.
-The happy-path tests (`test_close_long_in_profit`, `test_close_long_in_loss`, `test_close_short_in_profit`) check the same hand-computed payouts this chapter walked through. `test_liquidation_of_underwater_long` runs Dave's whole waterfall. When a future change breaks an invariant, the test that fails states, in its own name, which promise you just broke.
+The happy-path tests (`test_close_long_in_profit`, `test_close_long_in_loss`, `test_close_short_in_profit`) check the same hand-computed payouts this chapter walked through. `test_liquidation_of_underwater_long` runs Dave's whole waterfall. When a future change breaks an invariant, the failing test's name says which one.
## What You Learnt from Building the Perpetual Futures Venue
@@ -153,11 +150,11 @@ A traditional futures exchange is a matching engine, a clearing house with a mar
This chapter added four concepts:
-- **Pre-funded solvency.** Reserve every position's maximum payout at open, cap profit at the reservation, and gate LP withdrawals on free liquidity, so the pool can never owe more than it holds.
+- **Pre-funded solvency.** Reserve every position's maximum payout at open, cap profit at the reservation, and limit LP withdrawals to free liquidity, so the pool can never owe more than it holds.
- **Constant-time accounting.** Per-side scaled accumulators and entry snapshots make solvency, AUM, and settlement computable without ever iterating open positions: realizable credit, account-local safety, and bounded progress, in miniature.
- **Funding.** A pool-wide index plus a per-position entry snapshot charges the crowded side a fee, the same accounting structure every production venue uses.
- **Mark-to-market share pricing.** LP shares price against AUM including traders' unrealized PnL, so entering and leaving the pool is fair at any instant.
-This chapter used mints and token accounts, PDAs as vault authorities, shares priced against a pool, state rather than balances, and checks-effects-interactions without explaining any of them, because earlier chapters already had. The same goes for exits from every state, a signed external price checked before use, cranks run by profit-seekers, and integer arithmetic that rounds toward the house. In the *Where to Go From Here* chapter we will cover what this book left out, and how to take a program of your own from an idea to mainnet. The companion repository holds the code.
+This chapter used mints and token accounts, PDAs as vault authorities, shares priced against a pool, amounts read from state fields rather than vault balances, and checks-effects-interactions without explaining any of them, because earlier chapters already had. The same goes for a way out of every state for every participant, a signed external price checked before use, cranks run by profit-seekers, and integer arithmetic that rounds toward the house. In the *Where to Go From Here* chapter we will cover what this book left out, and how to take a program of your own from an idea to mainnet. The companion repository holds the code.
[^perps-volume]: [defillama.com](https://defillama.com) tracks spot and perpetual volumes side by side, so the gap between the two is one click away. It moves with the market.
diff --git a/building-financial-products-in-solana/prop-amm.md b/building-financial-products-in-solana/prop-amm.md
index e76a7eec..8af8ea3e 100644
--- a/building-financial-products-in-solana/prop-amm.md
+++ b/building-financial-products-in-solana/prop-amm.md
@@ -1,12 +1,12 @@
# Chapter 16: The Prop AMM
-The *Constant-Product AMM* chapter ended with the formula as the maker: one multiplication replacing the order book, at the cost of price impact and impermanent loss. This chapter builds the **proprietary AMM**, or prop AMM. A market-making firm funds a pool with its own capital and quotes both sides of it: anyone may buy the base token at the oracle price plus a spread, or sell it at the oracle price minus the spread. There is no curve, there are no liquidity providers, and there are no LP shares. The operator is the only capital in the market, which is the property the name comes from: *proprietary* trading is trading the firm's own money.
+The *Constant-Product AMM* chapter ended with the formula as the maker: one multiplication replacing the order book, at the cost of price impact and impermanent loss. This chapter builds the **proprietary AMM**, or prop AMM. A market-making firm funds a pool with its own capital and quotes both sides of it: anyone may buy the base token at the oracle price plus a spread, or sell it at the oracle price minus the spread. There is no curve, there are no liquidity providers, and there are no LP shares. The operator is the only capital in the market, which is the property the name comes from: *proprietary* trading is trading the firm's own capital.
Lifinity pioneered the model on Solana and wound down in December 2025, by which time the firms that followed it had taken most of its flow.[^lifinity-wind-down] By mid-2026 the venues doing that volume were BisonFi, HumidiFi, SolFi, ZeroFi, Tessera, and Obric, trading among the highest volumes on the network.[^prop-amm-share] You have probably never visited any of them, because they have no user interfaces. Their flow arrives through Jupiter, the aggregator that routes each swap to whichever venue quotes best, and a firm quoting live oracle prices with its own inventory usually quotes best. The program lives at `finance/prop-amm` in the companion repository.
## The Oracle Is the Price, the Spread Is the Fee
-A constant-product pool computes its price from its reserves, which means its price is only right when arbitrageurs have made it right, and every trade pays price impact along the way. A prop AMM inverts that. The price comes from an **oracle feed**, the machinery from the *Offchain Truth* chapter. The operator's only pricing decision is the **spread**: buyers of the base token pay the **ask**, the oracle price plus `spread_bps`; sellers receive the **bid**, the oracle price minus it. The bid and ask from the *Order Book Exchange* chapter are back, except no one is resting orders; the firm *is* both sides of the book, permanently, at whatever the oracle says plus its margin.
+A constant-product pool computes its price from its reserves, which means its price is only right when arbitrageurs have made it right, and every trade pays price impact along the way. A prop AMM inverts that. The price comes from an **oracle feed** account, like the one in the *Offchain Truth* chapter. The operator's only pricing decision is the **spread**: buyers of the base token pay the **ask**, the oracle price plus `spread_bps`; sellers receive the **bid**, the oracle price minus it. The bid and ask from the *Order Book Exchange* chapter are back, except no one is resting orders; the firm *is* both sides of the book, permanently, at whatever the oracle says plus its margin.
Two things are absent as a result:
@@ -23,9 +23,9 @@ Maria calls `initialize_market()` with a spread of 10 basis points and a 1% cap
Financial state on Solana is accounts, and this one transaction created the three the venue needs.
-The **market** account lives at a PDA seeded by the two mints: one market per pair, because the deployment *is* the firm; real prop AMMs are closed programs deployed by the firms themselves. The struct holds the operator's key, the oracle feed and its scale, both fixed at creation, the two token decimals (recorded at creation so the math never trusts a passed-in mint), `spread_bps`, a confidence limit, and a `paused` flag. The market PDA is the authority of the **base vault** and **quote vault**, and the program signs for them with the market's seeds.
+The **market** account lives at a PDA seeded by the two mints: one market per pair, because the deployment *is* the firm; real prop AMMs are closed programs deployed by the firms themselves. The struct holds the operator's public key, the oracle feed and its scale, both fixed at creation, the two token decimals (recorded at creation so the math never trusts a passed-in mint), `spread_bps`, a confidence limit, and a `paused` flag. The market PDA is the authority of the **base vault** and **quote vault**, and the program signs for them with the market's seeds.
-The market has no LP mint, because there are no depositors to issue receipts to. It has no fee ledger: the spread is the fee, and it stays in the vaults as inventory. And it has no virtual reserves, because pricing never reads the reserves. The *Fundraiser* chapter's rule was that balances are custody and state is truth. That rule exists because *other people* have claims against the vault that the balance alone cannot express. Here nobody but Maria has a claim on anything, so the vault balances are all the accounting the market needs.
+The market has no LP mint, because there are no depositors to issue receipts to. It has no fee ledger: the spread is the fee, and it stays in the vaults as inventory. And it has no virtual reserves, because pricing never reads the reserves. The *Fundraiser* chapter's program counts contributions in its own `current_amount` field rather than reading the vault balance, because *other people* have claims against the vault that the balance alone cannot express. Here nobody but Maria has a claim on anything, so the vault balances are all the accounting the market needs.
Then `deposit_inventory()` moves 1,000 NVDAx and 200,000 USDC from the firm's treasury into the vaults. No shares are minted, no deposit is recorded, and no `MINIMUM_LIQUIDITY` is withheld. Every deposit-side defense the *Constant-Product AMM* chapter needed exists to protect depositors from each other, and this market has one depositor.
@@ -39,7 +39,7 @@ Alice, carrying the standard wallet, wants 5 NVDAx. She calls `swap()` with the
- **The ask**: $165 plus 10 basis points is $165.165, **rounded up** in fixed point, because the buyer's price rounds against the buyer.
- **Her output**, floored: 825.825 USDC of her 1,000 buys exactly 5 NVDAx.
-Her `minimum_amount_out` guards the fill, and `InsufficientInventory` would reject the whole trade if the base vault could not cover it: a prop AMM never partially fills and never prices what it cannot deliver.
+The fill is checked against her `minimum_amount_out`, and `InsufficientInventory` would reject the whole trade if the base vault could not cover it: a prop AMM never partially fills and never prices what it cannot deliver.
Had a fund bought 500 NVDAx instead of Alice's 5, it would have paid $165.165 per token all the same. The *Constant-Product AMM* chapter's Bob paid an average of $2.46 on a $2.25 quote for moving the pool; Alice pays the quote.
@@ -69,7 +69,7 @@ Reading the price has its own cost, though. Between oracle updates, even fresh o
Her defense is `set_quote()`. When volatility rises, she widens the spread to 50 basis points (the next buyer pays $165.825 per NVDAx), and when her own view of the market diverges from the feed entirely, she sets `paused` and the market rejects every swap with `MarketPaused` until she returns. Watch the real venues during a fast market and you will see this: their quotes vanish from the aggregator and reappear minutes later, wider. Pulling quotes is routine for a market maker.
-The oracle gates pull the quote automatically, for the same reasons Maria pulls it by hand. A price older than 150 slots is refused (`StalePrice`), because a lagging quote is a free option for whoever notices first. A price whose confidence band exceeds the market's limit is refused (`OracleConfidenceTooWide`), because a tight spread around a number the oracle itself is unsure of will fill at prices well away from the real one, in whichever direction costs Maria money. For every other program in this book the staleness check protects users; for a market maker it protects the firm's own capital.
+The oracle checks in `swap()` pull the quote automatically, for the same reasons Maria pulls it by hand. A price older than 150 slots is refused (`StalePrice`), because a lagging quote is a free option for whoever notices first. A price whose confidence band exceeds the market's limit is refused (`OracleConfidenceTooWide`), because a tight spread around a number the oracle itself is unsure of will fill at prices well away from the real one, in whichever direction costs Maria. For every other program in this book the staleness check protects users; for a market maker it protects the firm's own capital.
## The Firm Chooses Whom It Fills
@@ -79,15 +79,15 @@ Some venues go further and keep a deny list of addresses they will not fill at a
## The Market Maker Leaves, Whole
-When Maria wants her capital back, `withdraw_inventory()` hands her both vaults, to the last minor unit, in one instruction, with nobody's permission. There is no share burn, no pro-rata slice, no locked floor, no waterfall. The exit machinery that filled three chapters exists to divide pooled money fairly among claimants, and this pool has one claimant. The market account survives, empty, refusing swaps with `InsufficientInventory` rather than mispricing them: every state needs an exit, and an empty market's exit is to refuse every swap.
+When Maria wants her capital back, `withdraw_inventory()` hands her both vaults, to the last minor unit, in one instruction, with nobody's permission. There is no share burn, no pro-rata slice, no locked floor, no waterfall. The withdrawal code of earlier chapters exists to divide pooled deposits fairly among claimants, and this pool has one claimant. The market account survives, empty, refusing swaps with `InsufficientInventory` rather than mispricing them, and it holds nobody's tokens, so nothing is stuck in it.
## The Math: Every Rounding Favors the Market
-The quote arithmetic is four small functions (`quote_math.rs`), all checked u128, multiply before divide, one floor per quantity. The ask rounds **up**; the bid rounds **down**; both outputs floor. Every rounding direction favors the market, so the composite invariant, that value out never exceeds value in measured at the raw oracle price, holds by construction. The handler still asserts it after computing each fill, cross-multiplied so the check itself does no rounding, and fails with `InvariantViolated` if a future refactor breaks a rounding direction. Assert after the math, as the constant-product pool's post-trade K check does, here guarding a much simpler formula.
+The quote arithmetic is four small functions (`quote_math.rs`), all checked u128, multiply before divide, one floor per quantity. The ask rounds **up**; the bid rounds **down**; both outputs floor. Every rounding direction favors the market, so the composite invariant holds: value out never exceeds value in, measured at the raw oracle price. The handler still asserts it after computing each fill, cross-multiplied so the check itself does no rounding, and fails with `InvariantViolated` if a future refactor breaks a rounding direction. Assert after the math, as the constant-product pool's post-trade K check does, here checking a much simpler formula.
The unit conversion is the only complicated part. Prices arrive at the oracle's fixed-point scale (10^8 here) while each token brings its own decimals. The program records both mints' decimals at creation and carries all three exponents through the arithmetic rather than assuming they cancel. With two six-decimal tokens they do cancel, which is why the chapter's numbers are clean; the program cannot assume its tokens will be.
-## The Invariants: What the Market Promises
+## The Invariants: What the Market Guarantees
- The quote brackets the oracle: bid at or below the price, ask at or above it, with each side's rounding favoring the market.
- No swap hands out more value, at the raw oracle price, than it takes in, enforced onchain after the math, per fill.
@@ -96,14 +96,14 @@ The unit conversion is the only complicated part. Prices arrive at the oracle's
## The Failure Modes: What Attacks a Market Maker
-- **The stale quote.** The catastrophic one, and it needs no exploit, just an oracle that lags a moving market. Every fill against a wrong quote is pure profit for the counterparty and pure loss for Maria, risk-free, which is why the staleness and confidence gates run before every swap (`test_swap_rejects_stale_price`, `test_swap_rejects_wide_confidence`). The *Perpetual Futures* chapter meets the same failure with leverage multiplying it.
+- **The stale quote.** The catastrophic one, and it needs no exploit, just an oracle that lags a moving market. Every fill against a wrong quote is pure profit for the counterparty and pure loss for Maria, risk-free, which is why `swap()` checks staleness and confidence before every fill (`test_swap_rejects_stale_price`, `test_swap_rejects_wide_confidence`). The *Perpetual Futures* chapter meets the same failure with leverage multiplying it.
- **Adverse selection.** Slower and subtler: even a fresh oracle trails the true price by seconds, and informed flow captures the difference. No `require!` can refuse "traders who know something"; the mitigations are commercial: a spread wide enough to pay for it, a ladder that charges for size, quotes pulled when the world is moving, and flow segmented by where it came from. The `paused` flag is a risk control Maria expects to use.
-- **The oracle is a chosen trust.** The operator picks the feed at creation, and the program deliberately does not verify the feed account's owner, the same visible trust decision the *Perpetual Futures* chapter makes, with one difference: a bad oracle here loses *Maria's* money, not her users'. A production reader should still verify the feed's owning program, and the companion repository's oracle module documents that upgrade to a verified read.
+- **The operator chooses which oracle to trust.** The operator picks the feed at creation, and the program deliberately does not verify the feed account's owner, the same visible trust decision the *Perpetual Futures* chapter makes, with one difference: a bad oracle here loses *Maria's* capital, not her users'. A production reader should still verify the feed's owning program, and the companion repository's oracle module documents that upgrade to a verified read.
- **What is absent.** Run the *Adversary* chapter's checklist against this program and whole categories return "not applicable". No inflation attack: there are no shares, so there is no share price for a first depositor to skew. No sandwich: no price impact to squeeze. No vacuous-invariant drain on an empty reserve: an empty vault rejects (`InsufficientInventory`) because inventory bounds the fill rather than pricing it. Each of those attacks needs a mechanism this program does not have.
## Testing Considerations
-The suite (`tests/test_prop_amm.rs`) checks the quote math to the minor unit in both directions: `test_swap_buys_base_at_the_ask` and `test_swap_sells_base_at_the_bid` assert Alice's and Bob's exact numbers, and `test_round_trip_costs_exactly_the_spread` asserts the 1.65 USDC that makes the spread the fee. `test_quote_follows_the_oracle` and `test_set_quote_changes_the_spread` cover the two ways the quote moves. The rejection tests cover every gate: stale price, wide confidence, paused market, slippage, zero amounts, inventory bounds, and the three operator-only handlers against an imposter. And `test_operator_can_withdraw_everything_and_swaps_then_fail` checks the rule that defines the genre: the capital is Maria's, and without it the market refuses every swap.
+The suite (`tests/test_prop_amm.rs`) checks the quote math to the minor unit in both directions: `test_swap_buys_base_at_the_ask` and `test_swap_sells_base_at_the_bid` assert Alice's and Bob's exact numbers, and `test_round_trip_costs_exactly_the_spread` asserts the 1.65 USDC that makes the spread the fee. `test_quote_follows_the_oracle` and `test_set_quote_changes_the_spread` cover the two ways the quote moves. The rejection tests cover every refusal: stale price, wide confidence, paused market, slippage, zero amounts, inventory bounds, and the three operator-only handlers against an imposter. And `test_operator_can_withdraw_everything_and_swaps_then_fail` checks that Maria can withdraw all of the inventory and that a swap afterwards fails: the capital is Maria's, and without it the market refuses every swap.
## What You Learnt from Building the Prop AMM
@@ -112,12 +112,12 @@ A traditional market-making desk quotes two-sided prices on an exchange, hedges
Five concepts came with the prop AMM:
- **Oracle-priced quoting.** The price comes from a feed, not from reserves, so there is no price impact and nothing to sandwich; the spread is the entire fee.
-- **For a market maker the oracle checks protect its own capital.** Staleness and confidence gates in front of every swap, because a lagging quote is a free option for whoever notices first.
+- **For a market maker the oracle checks protect its own capital.** `swap()` checks staleness and confidence before every fill, because a lagging quote is a free option for whoever notices first.
- **Adverse selection.** The flow that finds you when your quote is wrong is the flow you did not want: the market maker's structural cost, mitigated commercially (wider spreads, a size ladder, segmented flow, pulled quotes), never by a `require!`.
- **When balances legitimately are the accounting.** Every deposit-side defense earlier chapters needed exists to protect depositors from each other; with one depositor, the vault balances are the ledger.
- **Attack surface that was never built.** No shares, no curve, no fee ledger: entire classes from the *Adversary* chapter return "not applicable" because each of them needs a mechanism this design does not have.
-Traders accept the prop AMM's concentrated trust (one firm, one feed, one pause switch) because they leave nothing in its custody and see their price before they sign. In the *Lending and Borrowing* chapter we will build a program where users do leave their money in the program's custody, as supplied loans and as collateral, and the same kind of oracle values that collateral continuously. You will see what a lender has to do to stay solvent when the collateral falls, and who is paid to do it.
+Traders accept the prop AMM's concentrated trust (one firm, one feed, one pause switch) because they leave nothing in its custody and see their price before they sign. In the *Lending and Borrowing* chapter we will build a program where users do leave their tokens in the program's custody, as supplied loans and as collateral, and the same kind of oracle values that collateral continuously. You will see what a lender has to do to stay solvent when the collateral falls, and who is paid to do it.
[^lifinity-wind-down]: [solanafloor.com/news/lifinity-distribute-43m-token-holders-solana-original-prop-amm-winds-down](https://solanafloor.com/news/lifinity-distribute-43m-token-holders-solana-original-prop-amm-winds-down) reports the December 2025 governance vote that wound the venue down and returned its treasury to its token holders.
diff --git a/conclusion/where-to-go-from-here.md b/conclusion/where-to-go-from-here.md
index 9ed14435..4fc45711 100644
--- a/conclusion/where-to-go-from-here.md
+++ b/conclusion/where-to-go-from-here.md
@@ -1,6 +1,6 @@
# Chapter 21: Where to Go From Here
-Ten programs are behind you: an escrow, a fundraiser, a betting market, an order book, two automated market makers, a lending market, a managed fund, an options venue, and a perpetual futures venue. Between them they cover the machinery most financial products are made of.
+Ten programs are behind you: an escrow, a fundraiser, a betting market, an order book, two automated market makers, a lending market, a managed fund, an options venue, and a perpetual futures venue. Between them they cover the parts most financial products are built from.
The Introduction promised markets that never close, settlement that is atomic and global, one set of accounts that every party reads, rules that run as instruction handlers, fees that ignore the size of the transfer, and distribution that arrives the moment a program is deployed. Those are no longer sales points for a platform. They are materials you can build with.
@@ -10,13 +10,13 @@ What is left is deciding what to build with them.
You can take a financial product you understand in the real world and lay it out as accounts, handlers, and invariants, because you have already done it ten times.
-You can read somebody else's program and ask the right questions: which accounts are validated and which are trusted, who holds the authorities, what freezes at entry, and which state needs an exit handler. That is the same checklist an auditor opens with.
+You can read somebody else's program and ask the right questions: which accounts are validated and which are trusted, who holds the authorities, what freezes at entry, and whether every participant can get their assets out of every state. That is the same checklist an auditor opens with.
-You can treat an AI as a peer, not an authority. The companion AI plugin applies this book's rules to generated code, and you can now see when that output is wrong before it holds anyone's money.
+You can treat an AI as a peer, not an authority. The companion AI plugin applies this book's rules to generated code, and you can now see when that output is wrong before it holds anyone's tokens.
You can reason about who earns what, and why anyone shows up. Every chapter named the operator's incentive and the user's, because a mechanism nobody is paid to run does not run.
-And you can carry the responsibility that comes with all of it. Programs holding other people's money are not a place to find out whether an idea works.
+And you can carry the responsibility that comes with all of it. Programs holding other people's assets are not a place to find out whether an idea works.
## The World Still Runs on Business Hours
@@ -24,7 +24,7 @@ Look away from the chain for a moment, at what the people around you deal with e
A payment sent on Friday evening arrives on Tuesday, because the rails carrying it keep office hours and observe public holidays. A café hands a percentage of every sale to the card networks and processors, and on a four dollar coffee the fixed part of that fee often costs more than the percentage does. Someone sending money to family abroad pays a rate set by which two countries they happen to be standing in, which can reach as high as 20%.[^remittance-corridors] Money in a savings account earns close to nothing while the same money is lent out at several times that, and the gap between the two is the bank's business rather than a price the depositor could shop for.
-Businesses have the same problems from the other side. A shop owner who wants a second location either sells a slice of the business to whoever is nearby with money, or borrows through an underwriting process built for loans ten times larger. A supplier delivers goods and waits sixty days for the invoice to be paid while their own payroll runs every two weeks. Someone holding shares who needs cash for a repair sells them, ending a position they wanted to keep and often creating a tax bill, because borrowing against a portfolio is a private-banking service, and so requires a significant minimum balance. Rents rise with the value of housing, and the people paying the rent own none of it.
+Businesses have the same problems from the other side. A shop owner who wants a second location either sells a slice of the business to whoever is nearby with capital to invest, or borrows through an underwriting process built for loans ten times larger. A supplier delivers goods and waits sixty days for the invoice to be paid while their own payroll runs every two weeks. Someone holding shares who needs cash for a repair sells them, ending a position they wanted to keep and often creating a tax bill, because borrowing against a portfolio is a private-banking service, and so requires a significant minimum balance. Rents rise with the value of housing, and the people paying the rent own none of it.
Every one of these has the same structure: parties standing between two people who have business with each other, each taking a cut and adding a delay, and a set of hours during which the arrangement operates at all. A program does not remove the need for underwriting, and it does not make a stranger's credit risk disappear. What it removes is the middleman whose job was holding the money and keeping the record, which is the function that a vault and an account already perform, for a fraction of a penny, at three in the morning on a Sunday.
@@ -35,13 +35,13 @@ Nobody in the paragraphs above wants a blockchain. They want their money on Satu
So aim at consumers and businesses rather than at a crypto audience. In practice that means:
- Quote everything in dollars and in time, whether it is a rate, a limit, or a settlement time. Never basis points, token symbols, or pool utilization.
-- Keep keys and signing behind the product. Someone should be able to use it having never downloaded wallet software or held a token deliberately, and never learn that they now do.
+- Keep private keys and signing behind the product. Someone should be able to use it having never downloaded wallet software or held a token deliberately, and never learn that they now do.
- Put the benefit on the front page in the user's own words: "get paid the day you invoice", "spend without selling your shares", "nobody can walk off with the pot".
- Say the risk in the same plain words. Liquidation is not a footnote for somebody spending against their shares, and a rate that comes from borrowers can fall when borrowing does.
## What Products Worth Building Have in Common
-Products of this kind have been shipped already, several of them with real users and real money behind them. Study what they have in common rather than copying them line for line: each one names somebody specific, removes a cost or a delay that person never agreed to, and runs on machinery from a chapter of this book.
+Products of this kind have been shipped already, several of them with real users and real money behind them. Study what they have in common rather than copying them line for line: each one names somebody specific, removes a cost or a delay that person never agreed to, and runs on a program like one this book builds.
**An account that pays like a market and still handles traditional wires.** A business keeping its balances in USDC settles with anyone else inside the system in seconds, at any hour, and earns a rate that comes from what borrowers pay rather than from what a bank has left over. It still needs an IBAN for the supplier who can only receive a bank transfer, SWIFT for the ones abroad, and local rails for payroll. The product moves at the speed of the legacy system where it has to, and settles instantly everywhere else. The rate is the *Lending and Borrowing* chapter and the pooled accounting is the *Managed Fund* chapter. The hard parts are at the edges rather than in the middle: banking partners, the licenses that let money cross between the two systems, and the checks a bank is required to run on both sides of every transfer.
@@ -49,11 +49,11 @@ Products of this kind have been shipped already, several of them with real users
**Spending against your shares instead of selling them.** Someone holding NVDAx who needs two thousand dollars can sell, ending a position they wanted to keep and in many countries creating a tax bill, or borrow against it and keep it. The second option is the *Lending and Borrowing* chapter in full: shares as collateral, a price from a feed as covered in the *Offchain Truth* chapter, and a liquidation path when the collateral falls. Around it sits a card, a spending limit, and a warning that arrives long before a liquidator does.
-**A pot your friends collect when you skip a week.** Five friends stake fifty USDC each at the start of the month, and whoever misses their target forfeits a share to the ones who did not. That is the parimutuel pool of the *Betting Market* chapter: stakes in a vault, one settlement, winners split the pot, with the fact of who actually ran arriving the way every offchain fact does. Money in escrow is the oldest commitment device there is, and people go running when it is their own fifty in the pot.
+**A pot your friends collect when you skip a week.** Five friends stake fifty USDC each at the start of the month, and whoever misses their target forfeits a share to the ones who did not. That is the parimutuel pool of the *Betting Market* chapter: stakes in a vault, one settlement, winners split the pot, with the fact of who actually ran arriving the way every offchain fact does. Money in the pot is the oldest motivation device there is, and people go for a run when it is their own fifty at stake.
**A savings club with no bookkeeper.** Everyone pays a fixed amount in each month, one member takes the whole pot each round, and each member's turn comes once. Hundreds of millions of people run one, under a different name in each country.[^rosca-scope] It is a tanda in Mexico, a susu in West Africa and the Caribbean, a hui in China and Vietnam, and a chit fund in India. Economists call all of them a rotating savings and credit association, or ROSCA. The thing that goes wrong is always the organizer holding the pot between rounds. Contributions to a vault, the rotation as a state machine, the schedule against the clock as covered in the *Time* chapter, and the *Escrow* chapter's custody pattern underneath.
-**A rental deposit that pays its interest to the tenant.** A month or two of rent sits for a year in the landlord's account or an agency's, earning for whoever holds it. In a vault neither party controls it can pay the tenant instead, and still be released to either side by the rules. The design work is the state where the two sides disagree about damage, and giving that state an exit for both of them.
+**A rental deposit that pays its interest to the tenant.** A month or two of rent sits for a year in the landlord's account or an agency's, earning for whoever holds it. In a vault neither party controls it can pay the tenant instead, and still be released to either side when the lease ends. The design work is the state where the two sides disagree about damage, and making sure the deposit cannot be stuck there.
**Money raised from the people who already buy from you.** A roaster who needs eighty thousand dollars for a second location can raise it from the customers who drink the coffee: all-or-nothing against a deadline, refunds if the target is missed, repayment as a share of revenue. That is the *Fundraiser* chapter almost unchanged, and what it displaces is a chain of intermediaries, each with a minimum size below which the call is not worth returning.
@@ -63,15 +63,15 @@ Products of this kind have been shipped already, several of them with real users
**Payouts with no claim to file.** A flight lands three hours late; a region gets no rain for a month. Where the fact is measurable and comes from a source both sides agreed on in advance, the payout is a rule instead of a claims department: a pool that pays a fixed amount when the measurement crosses a line. Pool accounting from the *Betting Market* chapter, measurement from the *Offchain Truth* chapter. This is insurance in all but name, and insurance is licensed nearly everywhere. That makes it the plainest case of a rule that covers this whole page: what a product legally is follows from what it does for people, never from what it is built on.
-**Company cash that is not idle over the weekend.** Treasury cash that earns nothing from Friday to Monday can be held in a managed fund instead, redeemable at a share price at any hour. The *Managed Fund* chapter is the machinery, and who may change the mandate, and how fast, is the *Operating Financial Software* chapter's subject.
+**Company cash that is not idle over the weekend.** Treasury cash that earns nothing from Friday to Monday can be held in a managed fund instead, redeemable at a share price at any hour. The *Managed Fund* chapter builds that program, and who may change the mandate, and how fast, is the *Operating Financial Software* chapter's subject.
In each of these cases there is a specific situation, an individual or organization, and a cost or a delay they never agreed to. Solving those makes a good financial product.
## The Problems Nobody Has Solved Yet
-The better ideas are in the places where the machinery exists and no product has shipped.
+The better ideas are in the places where the programs exist and no product has shipped.
-**Rent that rises without you.** Rent tracks the housing market and a renter's savings do not, so every increase is a loss with nothing on the other side of it. Put a monthly amount into an asset that moves with the same market, and the asset's gain covers part of the next rise. The machinery is the *Managed Fund* chapter, deposits and a share price and a basket held against target weights, priced as the *Offchain Truth* chapter describes. The challenge is not the program. One city's rents and any tradable index of housing move together only loosely, and a renter told their rent is covered when it might not be is worse off than one never offered the product. The work is in the disclosure and the product design: saying how much of the next rise the asset is likely to cover, and how much it might not.
+**Rent that rises without you.** Rent tracks the housing market and a renter's savings do not, so every increase is a loss with nothing on the other side of it. Put a monthly amount into an asset that moves with the same market, and the asset's gain covers part of the next rise. The *Managed Fund* chapter's program already does this: it takes deposits, issues shares at a share price, and holds a basket against target weights, priced as the *Offchain Truth* chapter describes. The challenge is not the program. One city's rents and any tradable index of housing move together only loosely, and a renter told their rent is covered when it might not be is worse off than one never offered the product. The work is in the disclosure and the product design: saying how much of the next rise the asset is likely to cover, and how much it might not.
**Credit for people with no collateral to post.** Every program in this book lends against collateral worth more than the loan, because that is the only kind of credit a program can enforce by itself. The credit most people actually need is the other kind, and the things that make it work, an income record, a reputation, somewhere to take a defaulter, all live offchain.
@@ -83,9 +83,7 @@ Some things were left out on purpose.
**Vesting and streaming payments** have no chapter of their own, and the pay-on-approval product above is the argument for making one your first build.
-**Formal verification** appears here only briefly. The escrow program's formal proofs show what a machine-checked invariant looks like, but proving as a discipline, deciding what is worth proving and modeling the parts that cannot be executed symbolically, is a subject the size of this book.
-
-**Governance** is deliberately absent. Every program here has an operator with a key, and admin power is explained in the *Operating Financial Software* chapter. Replacing that key with a voting process is a separate design problem.
+**Governance** is deliberately absent. Every program here has an operator with a private key, and admin power is explained in the *Operating Financial Software* chapter. Replacing that private key with a voting process is a separate design problem.
**The interface** is the easiest thing this book left out. Nothing in these pages covers the screen someone actually taps, or the wallet connection, signing, and transaction building behind it, and all of that is ordinary application development, with libraries, examples, and plenty of people who already do it well. A good interface is what makes people adopt a financial product and keep using it, so give it real care. The hard part of this work is the program, and the program is behind you.
@@ -93,7 +91,7 @@ The book keeps changing after you close it. New chapters, corrections, and editi
## Build One, Then Have It Attacked
-One project separates reading this book from shipping, and it takes less time than you expect. Pick the smallest product you actually want to exist, for a person you can name. Draw its state machine before you write a handler. Give every state an exit, so every participant can get their money out from wherever the program stops. Write the tests that drive the machine down every path, especially the exits, and the invariant tests that assert what must never change.
+One project separates reading this book from shipping, and it takes less time than you expect. Pick the smallest product you actually want to exist, for a person you can name. Draw its state machine before you write a handler. Make sure every participant can get their assets out of every state the program can stop in. Write the tests that drive the machine down every path, especially the ones that pay tokens out, and the invariant tests that assert what must never change.
Then get a second (and third) opinion from an expert who did not work on it. Every check in this book's failure-mode sections exists because someone's first opinion missed something, and an auditor hunts the same holes an attacker would, then hands you the list instead of draining the program.
diff --git a/financial-programming-concepts-in-solana/figures/rent-lifecycle.svg b/financial-programming-concepts-in-solana/figures/rent-lifecycle.svg
index 8385b4b4..57fa9f55 100644
--- a/financial-programming-concepts-in-solana/figures/rent-lifecycle.svg
+++ b/financial-programming-concepts-in-solana/figures/rent-lifecycle.svg
@@ -27,7 +27,7 @@
canceled
- both exits close the accounts and refund every lamport to the maker
+ a take or a cancel closes both accounts, rent back to the maker
in a program with no cancel handler,
nothing closes an untaken offer's vault
diff --git a/financial-programming-concepts-in-solana/intro.md b/financial-programming-concepts-in-solana/intro.md
index 737a6e13..0008a8a6 100644
--- a/financial-programming-concepts-in-solana/intro.md
+++ b/financial-programming-concepts-in-solana/intro.md
@@ -1,6 +1,6 @@
# Chapter 6: State Machines
-This chapter covers the shape every financial program shares: a state machine, in which every participant can get their money out of every state, and every account returns its rent deposit when it closes.
+This chapter covers the shape every financial program shares: a state machine, in which every participant must be able to get their assets out of every state, and every account returns its rent deposit when it closes.
## Financial Software Is a State Machine
@@ -15,15 +15,15 @@ Before writing any handler, draw this machine: name the states, name every legal
Most programs are conceptually simple, with design rules that come straight from the state drawing:
-- **One instruction handler per transition.** `take_offer()` moves an offer from open to taken and does nothing else. Handlers that perform two transitions, or half of one, create bugs.
+- **Give each transition its own instruction handler.** `take_offer()` moves an offer from open to taken and does nothing else. Handlers that perform two transitions, or half of one, create bugs.
- **Make the state explicit.** A `status` enum on the account, or, often better, the account's own existence: the betting market's `Bet` account closes when the position ends, so "already claimed" means the account is gone. Using existence rather than a status is better since the state is recorded in one place rather than two. A state inferred from other fields is worse than either. The betting market could treat an empty pool as "still being set up", but then anyone's first bet would end the setup, so it records a Draft status that only the admin can change.
- **Check state, not just signers.** Betting on a settled event, canceling a filled order, and liquidating a healthy loan must fail because the *state* forbids them, with an error that names the rule.
-- **From every state, every participant must be able to get their money out.** Walking the states with this rule turns up the handler nobody wrote. An escrow with no cancel handler strands the maker's tokens the day nobody takes the offer. A betting market with no cancel-and-refund path strands every stake the day the event is called off. Walk each state and ask: how does each party's money leave from here? If any state has no answer, the program strands funds, and correct arithmetic will not fix that.
-- **An exit is not finished until nothing is left behind.** The vault is empty, the accounts are closed, and the rent is back with whoever paid it.
+- **Every participant must be able to get their assets out of every state.** Walking the states with this rule turns up the handler nobody wrote. If an escrow had no cancel handler, it would strand the maker's tokens if nobody took the offer. If a betting market had no cancel-and-refund path, it would strand every stake if the event was called off. Walk each state and ask: how do each party's assets leave from here? If any state has no answer, the program strands funds, and correct arithmetic will not fix that.
+- **Ensure the handler that pays out the last of the vault's tokens also closes everything.** It should empty the vault, close the vault and the state accounts, and return each account's rent deposit to whoever paid it.
## Whoever Opens an Account Pays Its Rent, and Gets It Back
-The last of the state-machine rules above says an exit returns the rent deposit. The *Core Building Blocks of Solana* chapter showed that every account holds one for the space it occupies. That raises two questions for a program that creates accounts on its users' behalf: who pays the deposit, and who gets it back when the account closes?
+Closing an account returns its rent deposit. The *Core Building Blocks of Solana* chapter showed that every account holds one for the space it occupies. That raises two questions for a program that creates accounts on its users' behalf: who pays the deposit, and who gets it back when the account closes?
- Whoever wants the account pays for it. The maker funds their own offer account and the vault to store the offered tokens; the taker funds their own token account (if it did not exist already) to receive what the maker offered. Rent burdens follow the party who chose to create the state.
@@ -33,5 +33,5 @@ The last of the state-machine rules above says an exit returns the rent deposit.
## What You Learnt
-- Every financial product is a small state machine: draw it before writing any handler, name every state and transition, and make sure every participant can get their money out of every state.
-- Give each transition its own handler, and check the account's state, not only who signed. An exit empties the vault, closes the accounts, and sends the rent deposit back to whoever paid it.
+- Every financial product is a small state machine: draw it before writing any handler, name every state and transition, and make sure every participant is able to get their assets out of every state.
+- Give each transition its own handler, and check the account's state, not only who signed. Ensure the handler that pays out the last of the vault's tokens also closes the vault and the state accounts, and returns each rent deposit to whoever paid it.
diff --git a/financial-programming-concepts-in-solana/offchain-truth.md b/financial-programming-concepts-in-solana/offchain-truth.md
index fb61ed7f..64ae099e 100644
--- a/financial-programming-concepts-in-solana/offchain-truth.md
+++ b/financial-programming-concepts-in-solana/offchain-truth.md
@@ -1,47 +1,47 @@
# Chapter 8: Offchain Truth
-A program can verify a lot on its own: signatures, balances, account ownership, its own arithmetic. But most financial products depend on facts that happen in the real world, outside anything the chain can see. Did the Federal Reserve cut rates this afternoon? Which film grossed highest this month? Did the shipment arrive? Did the borrower's collateral just crash? Every one of the book's markets turns on a fact like this: prices for the lending market and the perpetual futures exchange, an event outcome for the betting market, asset valuations for the managed fund. They all reach the program the same way: a key the program trusts vouches for the fact, and the program checks that fact's provenance, freshness, and confidence, and rejects degenerate values, before it uses it.
+A program can verify a lot on its own: signatures, balances, account ownership, its own arithmetic. But most financial products depend on facts that happen in the real world, outside anything the chain can see. Did the Federal Reserve cut rates this afternoon? Which film grossed highest this month? Did the shipment arrive? Did the borrower's collateral just crash? Every one of the book's markets turns on a fact like this: prices for the lending market and the perpetual futures exchange, an event outcome for the betting market, asset valuations for the managed fund. They all reach the program the same way: someone the program trusts signs the fact with their private key, and the program checks that fact's provenance, freshness, and confidence, and rejects degenerate values, before it uses it.
## An Oracle's Publication Is an Input to Check
-A fact becomes onchain truth when a key the program trusts has vouched for it. Publishers of such facts are called **oracles**, and what an oracle publishes reaches a program in one of two shapes.
+A program can act on a fact from outside the chain once someone it trusts has signed it with their private key. Publishers of such facts are called **oracles**, and what an oracle publishes reaches a program in one of two shapes.
-The first is **signed straight to you**: the trusted key signs the transaction carrying the fact. The betting market's admin settling an event is this, and the program checks a signer, and which key it is.
+The first is **signed straight to you**: the trusted party signs the transaction carrying the fact with their private key. The betting market's admin settling an event is this, and the program checks a signer, and which public key signed.
-The second is **published to an account**: the key signs its statement offchain, an oracle program verifies that signature onchain and records the fact in an account it owns, and your program reads that account in a later transaction. Pyth and Chainlink both work this way, and your own program verifies no signature at all. The signature was checked already, in somebody else's transaction. What your program establishes is where the account came from.
+The second is **published to an account**: the trusted party signs its statement offchain with their private key, an oracle program verifies that signature onchain and records the fact in an account it owns, and your program reads that account in a later transaction. Pyth and Chainlink both work this way, and your own program verifies no signature at all. The signature was checked already, in somebody else's transaction. What your program establishes is where the account came from.
Either way, by the time your program sees the fact it is an ordinary instruction argument or ordinary account data, and it can be held to the same standard as every other input:
-- **The provenance.** Is this the exact account the program was configured with, owned by the program whose verification you are relying on? A fact from an unexpected account is rejected. Anchor's `Account` enforces the owner and the discriminator, and the feed's own identity is a stored-pubkey comparison on top of that. The managed fund rejects any price account that is not the exact feed recorded in its registry; the lending market, which takes the first path, lets only the market owner's key write its feeds.
+- **The provenance.** Is this the exact account the program was configured with, owned by the program whose verification you are relying on? A fact from an unexpected account is rejected. Anchor's `Account` enforces the owner and the discriminator, and the feed's own identity is a stored-pubkey comparison on top of that. The managed fund rejects any price account that is not the exact feed recorded in its registry; the lending market, which takes the first path, lets only a transaction signed with the market owner's private key write its feeds.
- **The freshness.** A true price from five minutes ago can be a dangerously wrong price now. Every consumer states its tolerance and rejects anything older: the lending market refuses prices more than 25 slots old, the managed fund more than 60 seconds. Which clock a tolerance is stated in, and why the answer is usually slots, is the *Time* chapter's subject.
- **The confidence.** Good price feeds publish their own uncertainty: $250 ± $0.10 is a different fact from $250 ± $8, and the second one, during a volatile minute, may not be safe to liquidate against. A production consumer checks the confidence interval and rejects or degrades when it is too wide. A fresh price with a wide confidence interval is a separate failure mode from a stale one.
- **The degenerate values.** A price of zero or below means a broken feed, and the program must fail before that number multiplies against anything. Negative prices are real but exceptionally rare, and never for a token.[^negative-oil]
> **How a Pyth price reaches your program.** Publishers sign their prices, and the aggregate is attested by the signatures of Wormhole's guardians. Anyone can then post that signed update to Solana, where the **Pyth Receiver program** verifies those guardian signatures and writes a `PriceUpdateV2` account that it owns. Your program reads that account in a later transaction, and what it checks are `feed_id`, `publish_time`, and `conf`, never a signature, because the receiver already did that. One more field beyond the four checks above: `verification_level` records whether the full guardian set was checked or only some of its signatures, so a partially verified update is a weaker fact than a fully verified one. The companion repository's Pyth example reads these fields.
-An oracle's publication is *input*, as untrusted as an instruction argument until each of those checks has passed. A program that skips them is one substitution or one stale read away from a wrong valuation, and every health check, liquidation, and payout downstream then runs on the wrong number.
+An oracle update is as untrusted as any other input. The program must check it for substitution, staleness, uncertainty and degenerate values, or every health check, liquidation, and payout downstream runs on the wrong number.
## Trust Is an Engineering Parameter
Who signs the fact is a design decision with a range of answers, and the way to make it is to ask three questions: who publishes, what does it cost them to lie, and what happens when they stop publishing?
-At one end is the **single trusted key**. The betting market's admin settles the event with one signature; their word becomes the onchain truth. For a small market whose operator's business is its reputation, this can be the right design, and the program's job is to make the trust *visible and narrow*. The admin picks the winning outcome, but they cannot touch the pro-rata math, exceed the snapshotted fee, or reach the vault by any other path. A reader of the code can see what their signature can and cannot do.
+At one end is the **single trusted signer**. The betting market's admin settles the event with one signature: `settle_event()` writes their answer to the event account's `winning_outcome_index`, and every payout follows from it. For a small market whose operator's business is its reputation, this can be the right design, and the program's job is to make the trust *visible and narrow*. The admin picks the winning outcome, but they cannot touch the pro-rata math, exceed the snapshotted fee, or reach the vault by any other path. A reader of the code can see what their signature can and cannot do.
In the middle are **committees and multi-party resolution**: several parties must agree before a fact is recorded, so lying requires collusion and any single defector breaks the lie. The cost is coordination and latency.
At the far end are **price feed networks** like Pyth, which aggregate many independent publishers (exchanges, market makers) into a single signed publication with a confidence interval, updated continuously. Lying requires bribing a meaningful share of publishers whose businesses depend on being right, which is the strongest economic answer available to "what does it cost them to lie." This is what the lending market and perpetual futures exchange consume. Which network, and which crate decodes it, is a convention that lives in the companion repository; the checks in the previous section apply to all of them.
-The third question, **what happens when publication stops**, is the one designs forget. Feeds go stale during outages; committees deadlock; admins lose keys or interest. A program whose only paths require fresh truth is a program that can strand everyone's funds by hearing nothing. The staleness checks above handle the safety half (never act on old truth); the liveness half belongs to the state machine: an exit from every state that does not depend on the feed. The betting market can be canceled and refunded without an outcome ever being declared. A lending market whose oracle dies needs an escape hatch.[^escape-hatch] When you choose a trusted source, decide at the same time what the program does when it goes silent.
+The third question, **what happens when publication stops**, is an important one to include in your design. Feeds go stale during outages; committees deadlock; admins lose private keys or interest. A program in which every way to withdraw needs a fresh fact strands everyone's funds when the source goes quiet. The staleness checks above stop the program acting on an old fact. The state machine has to cover the other case: every participant must be able to get their assets out of every state without a new fact arriving. The betting market's admin can call `cancel_event()`, after which each bettor takes their stake back with `claim_refund()`, without an outcome ever being declared. A lending market whose oracle dies needs an escape hatch.[^escape-hatch] When you choose a trusted source, decide at the same time what the program does when it goes silent.
-One more rule applies: **the fact's meaning must be fixed when the money commits, not when the fact arrives.** The betting market fixes its outcome list when the admin opens it to bets, before any bet can land, and fixes its fee and its betting close time at event creation. The question bettors staked on cannot be rewritten before it is answered, and betting stops before the answer can be known.
+One more rule applies: **the fact's meaning must be fixed when participants commit their tokens, not when the fact arrives.** The betting market fixes its outcome list when the admin opens it to bets, before any bet can land, and fixes its fee and its betting close time at event creation. The question bettors staked on cannot be rewritten before it is answered, and betting stops before the answer can be known.
The price-manipulation section of the *Adversary* chapter covers the case these checks cannot catch: a signed fact that is fresh, confident, correctly verified, and *engineered*, a true report of a market the attacker just manipulated.
## What You Learnt
-- A fact is onchain truth because a key the program trusts vouched for it, either by signing your transaction directly or by signing offchain for an oracle program to verify and record in an account. Either way the publication is input: check its provenance, its freshness (in the right clock), its confidence, and its degenerate values.
-- Trust is an engineering parameter: a single key, a committee, or a feed network, chosen by asking who publishes, what lying costs them, and what happens when they go silent.
-- Fix the fact's meaning when the money commits, and give every state an exit that does not depend on the feed.
+- A program can act on a fact from outside the chain once someone it trusts has signed it with their private key, either by signing your transaction directly or by signing offchain for an oracle program to verify and record in an account. Either way the publication is input: check its provenance, its freshness (in the right clock), its confidence, and its degenerate values.
+- Treat trust as an engineering parameter: choose a single signer, a committee, or a feed network by asking who publishes, what lying costs them, and what happens when they go silent.
+- Fix the fact's meaning when participants commit their tokens, and make sure every participant is able to get their assets out of every state without waiting on a new fact.
[^negative-oil]: WTI crude oil futures traded as low as −$40.32 a barrel on 20 April 2020, the first negative price since the contract began trading in 1983, after storage filled and holders of the expiring May contract paid counterparties to take delivery off their hands. The EIA's account is at [eia.gov/todayinenergy/detail.php?id=43495](https://www.eia.gov/todayinenergy/detail.php?id=43495).
diff --git a/financial-programming-concepts-in-solana/operating-financial-software.md b/financial-programming-concepts-in-solana/operating-financial-software.md
index 7b7bc61e..8e62abeb 100644
--- a/financial-programming-concepts-in-solana/operating-financial-software.md
+++ b/financial-programming-concepts-in-solana/operating-financial-software.md
@@ -1,35 +1,37 @@
# Chapter 10: Operating Financial Software
-Correct code is only the start. The rest of the job is proving that code on every flow.
+Correct code is only the start. The rest of the job is testing it on every flow and running it safely once it holds other people's assets.
## Test the Edges of Every Interaction
-Financial code fails at the edges the happy path never reaches: the refund, the second claim, the withdrawal that lands exactly on the health boundary. A suite that covers only the happy path proves the demo works. The rules below push every test to an edge.
+Financial code fails at the edges the happy path never reaches: the refund, the second claim, the withdrawal that lands exactly on the health boundary. A suite that covers only the happy path checks only that the demo works. The rules below push every test to an edge.
-- **Build state through the program's own instruction handlers, never by injecting accounts.** A hand-built fixture skips the constraints under test, so a program can pass every fixture-based test and still be unusable, or unsafe, onchain. The one exception is an account a foreign program would have created, like a price feed. The harness builds that one as a fixture, so a test can set the price it needs.
+- **Build state through the program's own instruction handlers, never by injecting accounts.** A hand-built fixture skips the constraints under test, so a program can pass every test built on hand-made accounts and still be unusable, or unsafe, onchain. The one exception is an account a foreign program would have created, like a price feed. The harness builds that one as a fixture, so a test can set the price it needs.
- **Test the mirror image of every flow.** Withdraw as well as deposit, the ask as well as the bid, repay as well as borrow. The mirror is separate code, so a test of one copy says nothing about the other: a swap that works from USDC to NVDAx can fail from NVDAx to USDC.
- **Use nonzero, asymmetric values.** A test with `duration: 0`, or the same amount on both sides, sits at the one value where `<=` and `>=` both hold, so it passes with the comparison written backwards. Equal amounts hide a second bug: a formula with its operands swapped computes the same total either way. Use unequal amounts you can recognize in a failed assertion's output, like 1,000 USDC against 3 NVDAx, so a wrong total tells you which input it was built from.
- **Test both sides of every numerical boundary.** The withdrawal that lands exactly on the loan-to-value limit must succeed, and the same withdrawal plus one minor unit must fail. The lending suite's `withdraw_at_health_boundary_then_one_more_unit_fails` is that pair: a withdrawal that leaves borrowing power exactly equal to the debt passes, and one more minor unit is refused.
-- **Test every attack by name.** Every rule in a program chapter's failure-modes section gets a test that tries to break it: the non-maker canceling an offer, the fee vault passed in place of the user vault, the second claim on a settled event.
+- **Test every attack by name.** Every check in a program chapter's failure-modes section gets a test that tries to get past it: the non-maker canceling an offer, the fee vault passed in place of the user vault, the second claim on a settled event.
- **Assert invariants, not just account values.** Conservation (a round trip creates no value), monotonicity (an index never decreases), directional rounding (no path rounds in the user's favor). An invariant test catches a whole bug class; an assertion on one account's value catches one instance of it.
-Every test in this list is cheap to write and fast to run. The test runs the compiled program in-process in milliseconds, moving the clock forward makes weeks of interest accrue inside one test, and an injected feed account sets whatever price the test needs. Cashio and Wormhole, in the *Adversary* chapter, each lost their funds through a handler that accepted a substituted account. The fifth rule above turns that attack into a test: the fee vault passed in place of the user vault.
+Every test in this list is cheap to write and fast to run. The test runs the compiled program in-process in milliseconds, moving the clock forward makes weeks of interest accrue inside one test, and an injected feed account sets whatever price the test needs. Cashio and Wormhole, in the *Adversary* chapter, each lost their funds through a handler that accepted a substituted account. A test that passes the fee vault in place of the user vault runs that attack against your own program.
-## Proving Goes Further Than Testing
+## Model Checking Goes Further Than Testing
-A test checks the inputs you thought of. A proof checks all of them. Three levels of assurance run from one to the other, and how far up to go is set by how much money the program will hold:
+A test checks the values you thought of. A model checker checks all of them. Three levels of assurance run from one to the other, and how far up to go is set by how much the program's vaults will hold:
-- **Tests** drive every code path with values you chose, by the rules above.
-- **Invariant tests** assert a rule that must hold on every input (a round trip creates no value, an index never decreases) against generated inputs, so the values are no longer only the ones you imagined. The testing libraries call this property-based testing.
-- **Formal verification** hands a function to a model checker like Kani,[^kani] which explores *every* possible input, bit by bit, and either proves the invariant holds or hands you the exact input that breaks it.
+- **Tests** drive every code path with values you chose, at the edges listed above.
+- **Invariant tests** assert a condition that must hold for every value (a round trip creates no value, an index never decreases) against generated values, so the values are no longer only the ones you imagined. The testing libraries call this property-based testing.
+- **Model checking** hands a function to a model checker like Kani,[^kani] which tries *every* possible value of the function's inputs, bit by bit, and either reports that the invariant holds for all of them or hands you the exact values that break it.
-The escrow program in the companion repository ships Kani harnesses for the parts of it a proof can reach:
+Though Kani calls its checks proofs, a model check is not a formal proof. A formal proof is a mathematical argument a person writes in a proof assistant such as Lean, which the tool then checks step by step, and it can cover code a model checker cannot explore. The companion repository has model checks and tests, and no formal proofs.
-- `proof_take_offer_conserves_value` proves that taking an offer conserves the total of each token, on every input rather than only the tested ones.
-- `proof_cancel_offer_returns_all_to_maker` proves the refund path returns every vault token to the maker.
-- `proof_offer_id_seed_injective` proves two different offer IDs can never derive the same address.
+The escrow program in the companion repository ships Kani harnesses for the parts of it a model checker can reach. Kani marks a harness with `#[kani::proof]`, which is why the crate is called `kani-proofs` and the harnesses are named `proof_*`; each one is a model check:
-A handler that moves tokens through the Token Program cannot be model-checked end to end, because the cross-program invocation is a syscall the checker cannot step into. So the harness mirrors the program's arithmetic and statement order in pure functions and proves those. Percolator, Anatoly Yakovenko's Kani-verified perpetual-futures risk engine, is built the same way: a self-contained core the checker can explore whole. Each invariant in a program chapter's invariants section is a claim a harness like these can settle.
+- `proof_take_offer_conserves_value` checks that taking an offer conserves the total of each token, for every value rather than only the tested ones.
+- `proof_cancel_offer_returns_all_to_maker` checks that the refund path returns every vault token to the maker.
+- `proof_offer_id_seed_injective` checks that two different offer IDs can never derive the same address.
+
+A handler that moves tokens through the Token Program cannot be model-checked end to end, because the cross-program invocation is a syscall the checker cannot step into. So the harness mirrors the program's arithmetic and statement order in pure functions and checks those. Percolator, Anatoly Yakovenko's Kani-checked perpetual-futures risk engine, is built the same way: a self-contained core the checker can explore whole. Each invariant in a program chapter's invariants section is a claim a harness like these can check.
## Wait for Finality Before Doing Anything You Cannot Undo
@@ -43,31 +45,31 @@ The code that reads the chain has to answer that question, because it acts on wh
| **finalized** | The validators have voted the block final, and no fork can replace it | Nothing. This is the record |
- **A liquidator bot scans at `processed`**, because it is reading only to decide which transaction to send next, and a transaction aimed at a state that vanished fails.
-- **Anything that leaves the chain and cannot be recalled waits for `finalized`**: paying out fiat, emailing a customer that their money arrived, crediting a balance in your own books.
+- **Anything that leaves the chain and cannot be recalled waits for `finalized`**: paying out fiat, emailing a customer that their payment arrived, crediting a balance in your own books.
The rule underneath is to match the level to how reversible your reaction is, because the chain can drop a block that is not final, and your bank will not take the wire back. Waiting costs latency, and how much is set by the network's consensus rules, not by anything in your code.
## Admin Power Runs on a Written Specification
-Every market in this book has an operator with real power: withdrawing fees, settling events, setting prices, changing weights. Admin power is normal, and on Solana upgrade authority is expected, because a program has to be fixable when a bug turns up. The failure is *unbounded* power. Write the admin key a specification with three parts: what it can do, what it can never do, and what happens when it goes rogue or goes silent. The rules below are that specification, as the book's programs implement it.
+Every market in this book has an operator with real power: withdrawing fees, settling events, setting prices, changing weights. Admin power is normal, and on Solana upgrade authority is expected, because a program has to be fixable when a bug turns up. The failure is *unbounded* power. Write a specification for the admin's private key with three parts: what it can sign for, what it can never do, and what happens when its holder goes rogue or goes silent. The rules below are that specification, as the book's programs implement it.
- **Bound what configuration can express.** Validate every parameter at write time against a limit that makes abuse impossible by configuration. The managed fund refuses a management fee above 1,000 basis points a year, because `collect_fees` mints shares to the manager and dilutes every depositor, so an uncapped fee would drain the vault. The lending program refuses a loan-to-value ratio above the liquidation threshold, so no loan it grants is liquidatable the moment it is taken.
-- **Snapshot terms when money commits.** The betting market copies the config's fee onto each event when the event is created, along with the time betting closes, so the fee an event pays and the window bettors get are fixed before anyone can bet. The managed fund's fee has no setter at all. A parameter the admin can change applies to money that arrives after the change, never to money already inside.
-- **Give the admin no path to principal.** No instruction handler in the book's programs, under any parameters, moves user deposits to the operator. The way to make that checkable is to keep the operator's money in its own place. The order book's fee vault holds only taker fees, and `withdraw_fees` moves only from that vault to the market authority. The lending reserve keeps its share in `accumulated_protocol_fees`, which the exchange rate subtracts before valuing a supplier's shares, so collecting it changes nothing a supplier is owed. Rebalancing moves funds between program-owned vaults, and settlement pays by formula. When that holds in your program, say it in those words rather than claiming "non-custodial": the rules are the deployed program, and the operator is not in them.
-- **Pause entrances, never exits.** A pause that refuses new deposits and new positions during an incident protects users; one that refuses withdrawals traps their money. The prop AMM's `paused` flag, set through `set_quote`, makes `swap` refuse and is read by no other handler. Whatever a pause stops, every participant must still be able to get their money out of every state, paused included.
-- **Plan for the admin going silent.** Let nothing a user does wait on the operator: a deposit, a withdrawal, or canceling your own offer runs without an admin signature. The order book's `withdraw_fees` returns success with nothing to move when the fee vault is empty, so a scheduled job can call it every hour without failing. And add a way out for the day the price feed dies. A lending market that can no longer value collateral needs a wind-down path that closes positions and lets deposits leave, rather than freezing every user in place until a price arrives.
-- **Make upgrade authority worth trusting.** Ship fixes through a multisig with named holders rather than a single key on one person's laptop, and say so where users can read it. A user who reads that knows a change to the code takes several named people to sign, and that one stolen laptop cannot ship one.
+- **Snapshot terms when participants commit their tokens.** The betting market copies the config's fee onto each event when the event is created, along with the time betting closes, so the fee an event pays and the window bettors get are fixed before anyone can bet. The managed fund's fee has no setter at all. A parameter the admin can change applies to deposits that arrive after the change, never to deposits already inside.
+- **Give the admin no path to principal.** No instruction handler in the book's programs, under any parameters, moves user deposits to the operator. The way to make that checkable is to keep the operator's fees in their own place. The order book's fee vault holds only taker fees, and `withdraw_fees` moves only from that vault to the market authority. The lending reserve keeps its share in `accumulated_program_fees`, which the exchange rate subtracts before valuing a supplier's shares, so collecting it changes nothing a supplier is owed. Rebalancing moves funds between program-owned vaults, and settlement pays by formula. When that holds in your program, say it in those words rather than claiming "non-custodial": the rules are the deployed program, and the operator is not in them.
+- **Let a pause stop deposits and trades, never withdrawals.** A pause that refuses new deposits and new positions during an incident protects users; one that refuses withdrawals traps their deposits. The prop AMM's `paused` flag, set through `set_quote`, makes `swap` refuse and is read by no other handler. Whatever a pause stops, every participant must still be able to get their assets out of every state, paused included.
+- **Plan for the admin going silent.** Let nothing a user does wait on the operator: a deposit, a withdrawal, or canceling your own offer runs without an admin signature. The order book's `withdraw_fees` returns success with nothing to move when the fee vault is empty, so a scheduled job can call it every hour without failing. And plan for what happens if the price feed stops publishing. A lending market that can no longer value collateral needs a wind-down path that closes positions and lets deposits leave, rather than freezing every user in place until a price arrives.
+- **Make upgrade authority worth trusting.** Ship fixes through a multisig with named holders rather than a single private key on one person's laptop, and say so where users can read it. A user who reads that knows a change to the code takes several named people to sign, and that one stolen laptop cannot ship one.
## Ship To Mainnet With a Second (or Third) Opinion
-Before a program that holds other people's money touches mainnet, get a **second-opinion audit**: an independent reviewer, unattached to the team, walking the *Adversary* chapter's exploit classes against your code. Every check in the program chapters' failure-modes sections exists because someone's first opinion missed it. An auditor hunts for the same holes an attacker would, and hands you the list instead of draining the program.
+Before a program that holds other people's assets touches mainnet, get a **second-opinion audit**: an independent reviewer, unattached to the team, walking the *Adversary* chapter's exploit classes against your code. Every check in the program chapters' failure-modes sections exists because someone's first opinion missed it. An auditor hunts for the same holes an attacker would, and hands you the list instead of draining the program.
## What You Learnt
-- Bugs live at numerical boundaries, so a test suite pushes to every one: the mirror image of every flow, both sides of every limit, every attack by name, and invariants as well as account values.
-- Proving goes further than testing: invariant tests over generated inputs, then formal verification of the arithmetic core, and how far to go is set by how much money the program will hold.
-- A program cannot see whether its own block is final, so whatever reads the chain chooses a commitment level. A bot deciding which transaction to send next reads at `processed`, and a backend that pays out fiat or tells a customer their money arrived waits for `finalized`.
-- Admin power runs on a written specification: bounded config, snapshotted terms, and no path to principal. A pause stops entrances and leaves exits open, nothing a user does waits on the operator, and upgrade authority is held by a named multisig.
+- Bugs live at numerical boundaries, so push your test suite to every one: the mirror image of every flow, both sides of every limit, every attack by name, and invariants as well as account values.
+- Model checking goes further than testing: invariant tests over generated values, then a model checker over every value the arithmetic core can take, and how far to go is set by how much the program's vaults will hold.
+- A program cannot see whether its own block is final, so whatever reads the chain chooses a commitment level. A bot deciding which transaction to send next reads at `processed`, and a backend that pays out fiat or tells a customer their payment arrived waits for `finalized`.
+- Write admin power down as a specification: bounded config, snapshotted terms, and no path to principal. Let a pause stop deposits and trades but never withdrawals, make sure nothing a user does waits on the operator, and give upgrade authority to a named multisig.
- Ship with a second-opinion audit: an auditor hunts for the same holes an attacker would and hands you the list instead of draining the program.
[^kani]: Kani is a Rust model checker, at [github.com/model-checking/kani](https://github.com/model-checking/kani).
diff --git a/financial-programming-concepts-in-solana/the-adversary.md b/financial-programming-concepts-in-solana/the-adversary.md
index a14b59e2..7a0b00d5 100644
--- a/financial-programming-concepts-in-solana/the-adversary.md
+++ b/financial-programming-concepts-in-solana/the-adversary.md
@@ -2,9 +2,9 @@
The same half-dozen kinds of attack have drained onchain markets for a decade, on every chain and in every framework.
-## Every Mistake Is Money Anyone Can Take
+## Anyone Can Take the Tokens a Mistake Exposes
-Financial software on a public chain faces a harsher threat model than most software. Every instruction handler can be called by anyone, under any name, for a fraction of a cent. Your compiled program is public, and often your source is too. A mispriced trade, a leaked minor unit, or an unchecked account is money, and it goes to whoever finds it first.
+Financial software on a public chain faces a harsher threat model than most software. Every instruction handler can be called by anyone, under any name, for a fraction of a cent. Your compiled program is public, and often your source is too. A mispriced trade, a leaked minor unit, or an unchecked account lets tokens out of a vault, and they go to whoever finds the mistake first.
Three consequences follow:
@@ -33,7 +33,7 @@ A missing check lets the attacker **substitute** an account: pass a different ac
## The Empty Pool: Inflation and Donation Attacks
-Every share-based pool (vault, LP pool, lending reserve) prices a share as the pool's value divided by the number of shares. That formula breaks down at the start, when both numbers are tiny, and the **inflation attack** exploits it. The attacker makes a tiny first deposit and receives a tiny number of shares. Then they *donate* assets straight into the pool's vault with an ordinary token transfer, without calling the deposit handler. The donation raises the value behind each of their few shares. The next depositor's shares are computed as deposit × shares outstanding ÷ pool value, floored. With the pool value enormous compared with the shares outstanding, that division floors to zero or close to it: the victim's deposit buys nothing, and it now belongs to the attacker's shares.
+Every pool that issues shares (vault, LP pool, lending reserve) prices a share as the pool's value divided by the number of shares. That formula breaks down at the start, when both numbers are tiny, and the **inflation attack** exploits it. The attacker makes a tiny first deposit and receives a tiny number of shares. Then they *donate* assets straight into the pool's vault with an ordinary token transfer, without calling the deposit handler. The donation raises the value behind each of their few shares. The next depositor's shares are computed as deposit × shares outstanding ÷ pool value, floored. With the pool value enormous compared with the shares outstanding, that division floors to zero or close to it: the victim's deposit buys nothing, and it now belongs to the attacker's shares.
Take a pool that prices its shares from the token balance in its vault, as the book's managed fund did before it chose a defense. The first deposit into an empty pool mints one share per minor unit. Every later deposit mints shares in proportion to the value it adds, so a deposit that doubles the pool's value doubles the share supply:
@@ -68,7 +68,7 @@ The defenses make manipulation expensive and slow:
- **Time-weight the price.** A **time-weighted average price** (TWAP) over several minutes forces the attacker to hold the pushed price for that long, paying arbitrageurs the whole time, instead of paying for one slot of it.
- **Act on the confidence interval.** When a thin market is being pushed, its oracle's confidence interval widens. A program that pauses borrowing or tightens limits when that happens takes away the profit.
-- **Cap how much can move per window.** A limit on how much can be borrowed or withdrawn in each window forces the attacker to take the money over many slots, where people can see it happening and respond.
+- **Cap how much can move per window.** A limit on how much can be borrowed or withdrawn in each window forces the attacker to take the vault's tokens over many slots, where people can see it happening and respond.
- **Do not lend more against a token than it would cost to move its market.** If pushing the price costs less than the loan the push unlocks, someone will eventually do it.
## A Rounding Error Repeated Enough Times Drains the Pool
@@ -94,12 +94,12 @@ The *Arithmetic of Money* chapter's postcondition checks, such as re-checking K
## What You Learnt
-- Every mistake on a public chain is money anyone can take: attacks are automated, anyone can get their transaction ordered around yours, and attacks start at the extremes of a range.
-- Every supplied account is a claim to verify, and every chain of accounts must start from an account the program itself derived or recorded.
-- A share-based pool needs a defense against the inflation attack built into the program: locked shares, virtual shares, a pool size kept in program state, or a large minimum first deposit.
+- Anyone can take the tokens a mistake on a public chain exposes: attacks are automated, anyone can get their transaction ordered around yours, and attacks start at the extremes of a range.
+- Verify every supplied account, and start every chain of accounts from an account the program itself derived or recorded.
+- Build a defense against the inflation attack into any pool that issues shares: locked shares, virtual shares, a pool size kept in program state, or a large minimum first deposit.
- A true price can still be a manipulated price: time-weight it, act on the confidence interval, cap how much can move per window, and never lend more against a token than it would cost to move its market.
-- A rounding error in the user's favor gets repeated until the pool is empty, and a round-trip invariant test catches it.
-- Checks-effects-interactions at every boundary: your invariants must hold at the end of your own instruction, because the instructions after yours belong to someone else.
+- A rounding error in the user's favor gets repeated until the pool is empty, so write a round-trip invariant test to catch it.
+- Apply checks-effects-interactions at every boundary: your invariants must hold at the end of your own instruction, because the instructions after yours belong to someone else.
[^confused-deputy]: Norm Hardy named it in "The Confused Deputy (or why capabilities might have been invented)", ACM SIGOPS Operating Systems Review 22(4), October 1988, at [dl.acm.org/doi/10.1145/54289.871709](https://dl.acm.org/doi/10.1145/54289.871709).
diff --git a/financial-programming-concepts-in-solana/the-arithmetic-of-money.md b/financial-programming-concepts-in-solana/the-arithmetic-of-money.md
index b88b4e11..94642af2 100644
--- a/financial-programming-concepts-in-solana/the-arithmetic-of-money.md
+++ b/financial-programming-concepts-in-solana/the-arithmetic-of-money.md
@@ -1,10 +1,10 @@
# Chapter 7: The Arithmetic of Money
-Every bug class in this chapter has shipped in a real program, and the worst of them have drained real markets. The rules are few, and they follow from one fact: onchain money is integers.
+Every bug class in this chapter has shipped in a real program, and the worst of them have drained real markets. The rules are few, and they follow from one fact: onchain amounts are integers.
-## Money Is Integers: No Floats Near Funds
+## Amounts Are Integers: No Floats Near Funds
-Every validator that executes your program must compute exactly the same result, or the network cannot agree on state, so a program's float operations are emulated in software rather than run on the hardware. That makes them slow. They also accumulate representation error (0.1 has no exact binary form) and round to nearest, a direction nobody chose. Money itself is discrete anyway; there is no such thing as half a lamport. Representing quantities as whole integer steps is called **discretization**. For money it is exact, because money comes in whole units.
+Every validator that executes your program must compute exactly the same result, or the network cannot agree on state, so a program's float operations are emulated in software rather than run on the hardware. That makes them slow. They also accumulate representation error (0.1 has no exact binary form) and round to nearest, a direction nobody chose. Token amounts are discrete anyway; there is no such thing as half a lamport. Representing quantities as whole integer steps is called **discretization**. For token amounts it is exact, because every token comes in whole minor units.
So the rule is: token amounts, prices, rates, and everything derived from them are integers, u64 for amounts and u128 for intermediate math. No `f32`, no `f64`, no `powf`, no library that uses them internally. When a calculation needs fractions, scale integers up instead, as the WADs and Lots section below describes. When it needs a square root or an exponential, compute it with integer iteration: Newton's method for the square root, which the *Constant-Product AMM* chapter runs for real, and exponentiation by squaring for a power, both covered later in this chapter. None of the programs in this book uses a float, and your programs should not either.
@@ -28,7 +28,7 @@ Two conventions build on minor units:
Use checked arithmetic everywhere: `checked_add`, `checked_mul`, `checked_sub`, with a named overflow error. A plain `+` or `*` in a Rust release build **wraps**: when the result is too big for the type, it starts again from zero, so a `u64` balance one lamport past the maximum becomes a balance of almost nothing instead of an error. Overflow checks can be turned on in the build profile, but a financial program should not depend on a build setting.
-`saturating_*` clamps at the type's limit instead of failing, and that is right in one situation only: where the operand cannot go the wrong way by construction and the clamp is the answer you want. The programs in this book use it for a price feed's age, which floors at zero when the feed's slot is momentarily ahead of the local clock, and for the interest accrued since the last refresh, which cannot be negative because the factor only grows. On a value the accounting relies on it is worse than wrapping: `saturating_sub` turns 5 minus 10 into 0 rather than an error, so a debt that should have failed the calculation is recorded as no debt at all, and nothing anywhere reports a problem. The order book's fill accounting uses `checked_sub` there and says why in a comment: a silent clamp on the filled quantity would hide a remainder larger than the order, which is a bug the program must refuse rather than absorb.
+`saturating_*` clamps at the type's limit instead of failing, and that is right in one situation only: where a result past the limit has no meaning and the clamp is the answer you want. The programs in this book use it for a price feed's age, which floors at zero when the feed's slot is momentarily ahead of the local clock, and for the interest accrued since the last refresh, which cannot be negative because the factor only grows. On a value the accounting relies on it is worse than wrapping: `saturating_sub` turns 5 minus 10 into 0 rather than an error, so a debt that should have failed the calculation is recorded as no debt at all, and nothing anywhere reports a problem. The order book's fill accounting uses `checked_sub` there and says why in a comment: a silent clamp on the filled quantity would hide a remainder larger than the order, which is a bug the program must refuse rather than absorb.
## Multiply Before Divide, with Wider Intermediates
@@ -53,7 +53,7 @@ Is this greedy? Keeping the dust in the pool is the only policy that holds up: t
### Safety Checks Round Against the User Too
-The table covers money changing hands. The same direction applies to the numbers a program checks before it lets money move at all. A lending market floors collateral value and ceils debt, so a position always looks slightly worse than it is, never slightly better. The program refuses a loan the borrower can only just afford, rather than granting one they cannot quite repay.
+The table covers tokens changing hands. The same direction applies to the numbers a program checks before it lets tokens move at all. A lending market floors collateral value and ceils debt, so a position always looks slightly worse than it is, never slightly better. The program refuses a loan the borrower can only just afford, rather than granting one they cannot quite repay.
### A Ceiled Fee Is a Capped Fee
@@ -71,15 +71,15 @@ Over-repayment clamps to the debt, so nobody is overcharged either.
## Invariants and Conservation: Assert After the Math
-An **invariant** is a property that must hold before and after every state change, stated in code, checked at runtime. A check on the way in catches bad inputs; a check on the way out catches *your own bug*. The most useful invariants are conservation laws:
+An **invariant** is a condition that must hold before and after every state change, stated in code, checked at runtime. A check on the way in catches bad inputs; a check on the way out catches *your own bug*. The most useful invariants are conservation laws:
-- **Total in equals total out.** Every minor unit locked into an escrow, order lock, or bet pool is accounted for on every exit path: paid out, refunded, or taken as fee, summing exactly.
+- **Total in equals total out.** Every minor unit held in an escrow's vault, an order book's vaults, or a betting pool is accounted for on every path out of the program: paid out, refunded, or taken as fee, summing exactly.
- **The pool product must not decrease.** The AMM's x·y ≥ K check.
- **The vaults equal what the books owe.** Vault balances always cover the sum of every recorded obligation.
Check them **after the math, not just before.** The AMM re-checks its product after the transfers complete, and that check catches every bug above it that would overpay a user, including bugs in code that has not been written yet. The check costs a few hundred compute units; the drain an attacker could make without it is unbounded.
-One note from the same program: degenerate values like zero can make invariants hold even in dangerous situations. K is the product of the two reserves, so K is zero whenever either reserve is zero, even though the pool still holds every token of the other. In that state `new_k >= old_k` reads `new_k >= 0`, which every outcome satisfies, while the swap formula pays out the whole remaining reserve for any input at all. So the AMM guards the degenerate case directly, requiring both reserves positive before it prices anything.
+One note from the same program: degenerate values like zero can make invariants hold even in dangerous situations. K is the product of the two reserves, so K is zero whenever either reserve is zero, even though the pool still holds every token of the other. In that state `new_k >= old_k` reads `new_k >= 0`, which every outcome satisfies, while the swap formula pays out the whole remaining reserve for any input at all. So the AMM checks for the degenerate case directly, requiring both reserves positive before it prices anything.
## One Accumulation Factor Compounds Interest for Every Borrower
@@ -91,18 +91,18 @@ The **accumulation factor** removes the loop. Each market stores one fixed-point
new factor = old factor × (1 + rate per second × elapsed seconds)
```
-Each loan stores only its principal, and a live debt is that principal multiplied by the current factor. One number moves instead of thousands, interest compounds across updates on its own, and no tokens move until someone repays or withdraws. The lending example calls the field `borrow_accumulation_factor`; the *Lending and Borrowing* chapter runs it.
+Each loan stores only its principal, and a live debt is that principal multiplied by the current factor. One number moves instead of thousands, interest compounds because each update multiplies the factor by that period's growth, and no tokens move until someone repays or withdraws. The lending example calls the field `borrow_accumulation_factor`; the *Lending and Borrowing* chapter runs it.
That update is an approximation of compounding, and it is the one the lending market runs. The exact form exists for the day a formula needs it:
-- **The everyday update approximates.** Multiplying by (1 + rate × elapsed seconds) is simple interest over the interval. True compounding over n seconds is (1 + rate)ⁿ, a power; the approximation is a multiply. The lending market grows `borrow_accumulation_factor` this way in `refresh_reserve()`, and every handler that moves money requires that refresh to have run first, so the intervals stay short. With a tiny per-second rate and short intervals, the difference from true compounding is smaller than anything a borrower could measure.
+- **The everyday update approximates.** Multiplying by (1 + rate × elapsed seconds) is simple interest over the interval. True compounding over n seconds is (1 + rate)ⁿ, a power; the approximation is a multiply. The lending market grows `borrow_accumulation_factor` this way in `refresh_reserve()`, and every handler that moves tokens requires that refresh to have run first, so the intervals stay short. With a tiny per-second rate and short intervals, the difference from true compounding is smaller than anything a borrower could measure.
- **When a formula needs the exact power, square.** For (1 + rate)ⁿ with a large n, do not multiply n times. Use repeated squaring instead: (1 + rate)⁸ is three squarings rather than seven multiplications, and in general about log₂(n) of them, each with checked math and floored the same way. This is **exponentiation by squaring**. No program in this book needs it, because the lending market refreshes often enough that the approximation stays closer than the rounding. It is the tool for a market that can sit untouched for a long stretch, where one catch-up update would otherwise treat months of interest as simple interest.
The rate itself is a policy choice; the arithmetic above only applies whatever rate the market sets. Nearly every lending market sets it the same way, though: as a function of **utilization**, the share of the pool that is lent out. The rate rises gently up to a target utilization and steeply beyond it, so a pool that is nearly empty pays depositors more and charges borrowers more until deposits come back or loans are repaid.
## What You Learnt
-- Onchain money is integers: no floats near funds, every amount at a declared scale read from the mint, lots for order books and WADs for fractions.
+- Keep onchain amounts as integers: no floats near funds, every amount at a declared scale read from the mint, lots for order books and WADs for fractions.
- Multiply before divide, promote intermediates to u128, and check every operation with a named overflow error.
- Rounding is a policy decision: floor what the program pays, ceil what the user owes, round once, and keep health checks conservative.
- Assert invariants after the math, and know the degenerate values that let an invariant hold in a dangerous state.
diff --git a/prelude/introduction.md b/prelude/introduction.md
index f47c801c..046d35cb 100644
--- a/prelude/introduction.md
+++ b/prelude/introduction.md
@@ -11,8 +11,8 @@ Financial products built on Solana have properties no traditional payment system
- **Markets are tradable 24 x 7.** No opening bell, no weekend close, no holiday calendar.[^after-hours]
- **Settlement is atomic, instant, and global.** A trade and its settlement can be the same event, whether the two parties are in the same city or on different continents, and no failure leaves one side paid and the other empty-handed.
- **Everyone reads the same accounts.** A traditional trade is recorded in two firms' separate books, and departments exist to check the copies against each other. Onchain there are no copies: a market's state is its accounts, and every party reads the same ones. There is nothing to reconcile.
-- **Money is programmable.** The rules an institution enforces with staff and procedure (release the escrow when both sides have delivered, accrue the interest, liquidate the underwater loan, pay the winners their share) run as instruction handlers: code anyone can invoke and nobody has to remember. Every chapter of this book builds rules like these.
-- **Transactions cost a fraction of a penny**, regardless of the size of the transfer. Moving money the traditional way costs a percentage of it. Think back to the last time you bought a coffee: the café that sold it paid roughly 1.5% to 3.5% of the price in card fees, plus a fixed 10 to 30 cents. Sending $200 across a border costs under 1% through a digital service like Wise, and up to 20% at a Western Union or MoneyGram cash counter in some parts of the world.[^remittance-scope] A flat fee changes what products are worth building.
+- **Assets are programmable.** The rules an institution enforces with staff and procedure (release the escrow when both sides have delivered, accrue the interest, liquidate the underwater loan, pay the winners their share) run as instruction handlers: code anyone can invoke and nobody has to remember. Every chapter of this book builds rules like these.
+- **Transactions cost a fraction of a penny**, regardless of the size of the transfer. Moving dollars the traditional way costs a percentage of the amount. Think back to the last time you bought a coffee: the café that sold it paid roughly 1.5% to 3.5% of the price in card fees, plus a fixed 10 to 30 cents. Sending $200 across a border costs under 1% through a digital service like Wise, and up to 20% at a Western Union or MoneyGram cash counter in some parts of the world.[^remittance-scope] A flat fee changes what products are worth building.
- **Blockchains can move assets of any kind.** As of mid-2026, Solana is the most popular blockchain for trading real-world assets: equities, bonds, commodities, real estate, and exotics.[^rwa-scope] Solana is also one of the most active chains for trading crypto-native assets such as BTC.[^btc-scope]
- **A deployed program reaches every user at once.** A traditional fund spends months working through intermediaries before it reaches investors: an exchange listing, market makers, brokerage approvals. Each gatekeeper adds cost and delay. A deployed program is reachable by every wallet on the network, and by every other program, the moment it exists.
@@ -28,7 +28,7 @@ Solana changes quickly: network releases, framework versions, chain constants, a
*Building Financial Software on Solana* teaches the durable layer: **how financial ideas become Solana programs, implemented as accounts, instruction handlers, signatures, oracles, checks and math.** The account model has been stable since Solana launched. The math will not change. Nor will the desire of adversaries to take your assets.
-In this book you'll learn how each of the classic financial products works as a Solana program, how to handle assets without losing a penny, and how to prove your program works.
+In this book you'll learn how each of the classic financial products works as a Solana program, how to handle assets without losing a penny, and how to test that your program works.
## Why Write a Book When an AI Can Write the Program?
@@ -55,7 +55,7 @@ This book does not assume you have written a Solana program, and it does not ass
Every Solana mechanism is introduced before it is used, starting from the basics of users, accounts, programs, and data. Every financial concept (escrow, collateral, liquidation, market making, interest) is explained from first principles, through named people with real dollar amounts using the program in realistic scenarios: making gains, taking losses, and occasionally trying to attack it.
-It does not assume you are a software engineer, or that you know Rust, the programming language Solana programs are written in. The behavior of every program is explained in detail: the accounts it stores, the instruction handlers that move its money, what each handler checks before it does, and what happens when a check is missing. Handler and field names appear in the text so that the explanation and the code can be read side by side. Software engineers will find a complete implementation of every program in the companion repository, in Anchor 1, Anchor 2, and Quasar, with further languages and frameworks added as they arrive. Its tests run continuously, so the code stays current in a way a printed book cannot.
+It does not assume you are a software engineer, or that you know Rust, the programming language Solana programs are written in. The behavior of every program is explained in detail: the accounts it stores, the instruction handlers that move its tokens, what each handler checks before it does, and what happens when a check is missing. Handler and field names appear in the text so that the explanation and the code can be read side by side. Software engineers will find a complete implementation of every program in the companion repository, in Anchor 1, Anchor 2, and Quasar, with further languages and frameworks added as they arrive. Its tests run continuously, so the code stays current in a way a printed book cannot.
The diagrams throughout the book share one set of conventions, and each convention is explained by the first figure that uses it.
@@ -65,13 +65,13 @@ All technical and financial vocabulary this book uses is defined in the Glossary
## Programs Follow Patterns, Patterns Build Understanding
-The book has two parts. **The Fundamentals** is everything you need before writing financial software: the Solana mechanics first (accounts, tokens, PDAs, rent, atomicity, and time), then what is specific to money (the arithmetic, offchain truth, the adversary, testing, and admin power).
+The book has two parts. **The Fundamentals** is everything you need before writing financial software: the Solana mechanics first (accounts, tokens, PDAs, rent, atomicity, and time), then what is specific to financial software (the arithmetic, offchain truth, the adversary, testing, and admin power).
-**Building Financial Products** is ten chapters, and each one builds a complete financial program. A Solana program is code deployed onchain, and other blockchains call the same thing a "smart contract" or a "protocol". Between them the ten cover the financial products commonly built on Solana: escrow, crowdfunding, a betting market, an order book exchange, both kinds of automated market maker, lending and borrowing, a vault, options, and perpetual futures. Each chapter gives the accounts the program stores, the instruction handlers that move its money, the incentives of the people who use it and of whoever administers it, and the ways it can be attacked. Each chapter is written and drawn in the same visual language: account maps, state machines, and value flows.
+**Building Financial Products** is ten chapters, and each one builds a complete financial program. A Solana program is code deployed onchain, and other blockchains call the same thing a "smart contract" or a "protocol". Between them the ten cover the financial products commonly built on Solana: escrow, crowdfunding, a betting market, an order book exchange, both kinds of automated market maker, lending and borrowing, a vault, options, and perpetual futures. Each chapter gives the accounts the program stores, the instruction handlers that move its tokens, the incentives of the people who use it and of whoever administers it, and the ways it can be attacked. Each chapter is written and drawn in the same visual language: account maps, state machines, and value flows.
Real incidents appear throughout the book in boxed **War Story** sections. None of them is told to embarrass the teams involved; they are here so you recognize the same mistake in your own code before it costs you.
-To keep the money easy to track, every character starts every chapter with the same wallet: **one SOL** for rent deposits and transaction fees, **one thousand USDC**, and whatever story-specific assets the chapter hands them (a share grant, a stock position, a token inventory).
+To keep the balances easy to track, every character starts every chapter with the same wallet: **one SOL** for rent deposits and transaction fees, **one thousand USDC**, and whatever story-specific assets the chapter hands them (a share grant, a stock position, a token inventory).
## How This Book Was Produced
@@ -88,7 +88,7 @@ Follow @QuicknodeSolana on X for updates to the book, new chapters and editions
## Where to Start, Depending on What You Came For
- **A software engineer who wants to build a complete financial product?** Read [Part I](../TABLE-OF-CONTENTS.md#part-i-the-fundamentals) front to back, chapter by chapter, and decide on a framework before you finish it. The companion repository ships every program in Anchor 1, Anchor 2, and Quasar, and its front page describes each one and how to install it. When you reach [Part II](../TABLE-OF-CONTENTS.md#building-financial-products-in-solana), open each chapter's project in the companion repository in the framework you chose, and read the chapter and the code side by side. The chapter explains what each instruction handler checks and moves, and the handler and its tests show how, under the same names.
-- **A software engineer who wants to build one specific thing?** Read [Part I](../TABLE-OF-CONTENTS.md#part-i-the-fundamentals), then the [*Escrow*](../building-financial-products-in-solana/escrow.md) chapter, which teaches the vault, the stored terms, and the exits that every later program reuses. Then skip ahead to [your program's chapter](../TABLE-OF-CONTENTS.md#building-financial-products-in-solana), with its project from the companion repository open beside it. Where that chapter leans on an earlier one, it names the earlier chapter at the point it does.
+- **A software engineer who wants to build one specific thing?** Read [Part I](../TABLE-OF-CONTENTS.md#part-i-the-fundamentals), then the [*Escrow*](../building-financial-products-in-solana/escrow.md) chapter, which teaches the vault, the stored terms, and handlers (`take_offer()` and `cancel_offer()`) that empty the vault from every state; every later program reuses all three. Then skip ahead to [your program's chapter](../TABLE-OF-CONTENTS.md#building-financial-products-in-solana), with its project from the companion repository open beside it. Where that chapter leans on an earlier one, it names the earlier chapter at the point it does.
- **Here to understand how these products work on Solana, not to build one?** An analyst, a risk or compliance officer, a product manager, a lawyer, a founder deciding what to fund: anyone whose work touches onchain finance without writing its code. Read [Solana Building Blocks](../TABLE-OF-CONTENTS.md#solana-building-blocks) for the vocabulary, then the [*Offchain Truth*](../financial-programming-concepts-in-solana/offchain-truth.md) and [*Adversary*](../financial-programming-concepts-in-solana/the-adversary.md) chapters, then the [program chapters](../TABLE-OF-CONTENTS.md#building-financial-products-in-solana) in order. Each program chapter tells its story with named people and dollar amounts, says who earns what and why, and ends with how the program can be attacked, and none of that needs the code. The [*Arithmetic of Money*](../financial-programming-concepts-in-solana/the-arithmetic-of-money.md) and [*Operating Financial Software*](../financial-programming-concepts-in-solana/operating-financial-software.md) chapters are written for whoever writes the program, so you can skim them.
Part I opens with the computer itself: one giant machine that spans the world, and two people, Alice and Bob, using it to pay each other. Let's get started.
diff --git a/scripts/lint-manuscript.mjs b/scripts/lint-manuscript.mjs
index 52304c98..87582929 100644
--- a/scripts/lint-manuscript.mjs
+++ b/scripts/lint-manuscript.mjs
@@ -172,17 +172,65 @@ const rules = [
[/can(?:not|'t) verify the price/i, "negative offchain-truth framing"],
[/\bBONK\b/, "no meme tokens; use USDC and RWA stocks (NVDAx, TSLAx, SPCXx)"],
[/\bmemecoin|meme token/i, "no meme tokens"],
+ // A label that sounds like a term of art and names nothing: say which program,
+ // handler, instructions or software is meant.
+ [/\bmachinery\b/i, 'name the program, handler or software instead of "the machinery"'],
[/\bsmart contracts?\b/i, 'say "program" (the only allowed uses are the contrastive mentions in the Introduction and the glossary)'],
// "protocol" names the network and the programs on it, so it tells a reader
- // neither. Identifiers that ship with the word keep it: the lookarounds skip
- // anything backticked or joined by an underscore.
- [/(? {
- for (const [pattern, reason] of rules) {
- if (pattern.test(line) && !exceptions.some((ok) => ok.test(line))) {
+ for (const [pattern, reason, ruleException] of rules) {
+ if (pattern.test(line) && !(ruleException && ruleException.test(line)) && !exceptions.some((ok) => ok.test(line))) {
failures += 1;
console.error(`${file}:${index + 1}: ${reason}\n ${line.trim().slice(0, 160)}`);
}
diff --git a/solana-building-blocks/accounts-and-instruction-handlers.md b/solana-building-blocks/accounts-and-instruction-handlers.md
index 0db586c3..4a05d274 100644
--- a/solana-building-blocks/accounts-and-instruction-handlers.md
+++ b/solana-building-blocks/accounts-and-instruction-handlers.md
@@ -26,19 +26,19 @@ A **wallet** is a key pair being used to sign transactions, with the private key
The curve in the figure is labeled **Solana devnet**, because there is more than one Solana computer. Each running instance is a **cluster**: the same software, a separate universe. What appears at an address in one cluster can be (and usually is) completely different to what appears at the same address in a different cluster.
-| Cluster | Purpose | Assets have value? | Where it runs |
-| ------------ | --------------------------------------------- | ------------------- | ---------------------------- |
-| **mainnet** | Production: where real financial products run | Yes | Globally |
-| **devnet** | Identical machinery for building and testing | No: tokens are free | Globally |
-| **localnet** | Development and fast test loops | No | Locally, on your own machine |
+| Cluster | Purpose | Assets have value? | Where it runs |
+| ------------ | ------------------------------------------------------------------- | ------------------- | ---------------------------- |
+| **mainnet** | Production: where real financial products run | Yes | Globally |
+| **devnet** | A global network where developers deploy their in-progress software | No: tokens are free | Globally |
+| **localnet** | Development and fast test loops | No | Locally, on your own machine |
-There is also **testnet**, where future versions of the Solana software itself are tested before a release reaches mainnet. Other clusters are started up now and then to try out a single new feature. Those exist to test the network itself. Your own work happens almost entirely on localnet and devnet: localnet for the fast build-and-test loop, then devnet for a rehearsal on the same machinery real users will meet. Mainnet is where a finished program goes live. Assets there have real value, which is why a program should be thoroughly audited before it goes live there.
+There is also **testnet**, where future versions of the Solana software itself are tested before a release reaches mainnet. Other clusters are started up now and then to try out a particular proposed network feature. Your own work happens almost entirely on localnet and devnet: localnet for the fast build-and-test loop, then devnet for a rehearsal on the same software real users will meet. Mainnet is where a finished program goes live. Assets there have real value, which is why a program should be thoroughly audited before it goes live there.
Each cluster is reached at a URL. Programs and wallets never talk to the network directly: they send transactions to an **RPC endpoint**, a server that holds the cluster's state and relays transactions into it. The endpoint URL is the setting that decides which Solana cluster software the Solana SDK uses to read, write, and deploy to.
The Solana Foundation runs a public endpoint for each cluster, rate limited and meant for learning and light development; anything with real users runs against a dedicated endpoint from an RPC provider instead. Quicknode, this book's publisher, is one of several. We, and most benchmarks, would also say it's the best!
-Alice's SOL is devnet SOL: think of it as the pretend money from a board game: it feels like real money inside the game but buys nothing outside it. Devnet tokens cost nothing: devnet SOL, devnet USDC and other devnet tokens are given away free.[^faucets] Alice's address is the same on every cluster; the *account* at her address exists per cluster, and nothing in this book needs real money to work through.
+Alice's SOL is devnet SOL: think of it as the paper banknotes from a board game: they feel real inside the game but buy nothing outside it. Devnet tokens cost nothing: devnet SOL, devnet USDC and other devnet tokens are given away free.[^faucets] Alice's address is the same on every cluster; the *account* at her address exists per cluster, and nothing in this book needs mainnet tokens to work through.
## Receiving Doesn't Need A Signature, Spending Does
@@ -69,7 +69,7 @@ Every instruction names which of the program's **instruction handlers** to run,
Alice runs her first transaction. It carries two instructions: the first tells the System Program's `transfer()` handler to move 0.05 SOL from her account to Bob's, and the second tells the Memo Program to write "thanks" to the log.
-The transfer instruction does not actually say 0.05 SOL. It says 50,000,000 **lamports**, SOL's **minor unit**: one billion lamports make one SOL, the way one hundred cents make one dollar. Balances are stored as, and calculated with, integers of lamports, so the balance an explorer displays as 1 SOL is the integer 1,000,000,000. Every asset in this book gets the same treatment. The *Arithmetic of Money* chapter builds the rest of the book's money math on this rule.
+The transfer instruction does not actually say 0.05 SOL. It says 50,000,000 **lamports**, SOL's **minor unit**: one billion lamports make one SOL, the way one hundred cents make one dollar. Balances are stored as, and calculated with, integers of lamports, so the balance an explorer displays as 1 SOL is the integer 1,000,000,000. Every asset in this book gets the same treatment. The *Arithmetic of Money* chapter builds the rest of the book's arithmetic from these integer minor units.
> **Why "lamport"?** Lamports are named after Leslie Lamport, the Turing Award-winning computer scientist whose work on time, ordering, and consensus in distributed systems underpins every blockchain, Solana included.
@@ -83,7 +83,7 @@ For financial software, atomicity is a primitive you design with. Inside a trans
- the escrow program you're about to build moves Alice's payment and Bob's stock in one atomic unit, and no combination of failures leaves one side paid and the other empty-handed.
- A lending market can have an instruction to refresh its price before another instruction that depends on the price, so it does not act on stale state
-- A **flash loan**, a crypto specific concept, borrows in a transaction's first instruction, uses the money in the middle, and repays in the final instruction. If the repayment fails, the borrow itself is unwound: the lender risked nothing, so the loan needs no collateral.
+- A **flash loan**, a crypto specific concept, borrows in a transaction's first instruction, uses the borrowed tokens in the middle, and repays in the final instruction. If the repayment fails, the borrow itself is unwound: the lender risked nothing, so the loan needs no collateral.
Whole categories of arbitrage and liquidation strategies exist only because transactions are all-or-nothing.
@@ -95,7 +95,7 @@ The budget for work done by instructions is metered in **compute units** per tra
One thing made Alice's transaction valid: before sending it, she signed it with her private key. The signature is cryptographic proof that the holder of whatever private key matches this public key authorized this exact transaction, not a similar one and not an edited one. Anyone can use Alice's public key to verify a signature that claims to be from Alice.
- You already use digital signatures daily: your bank card has a private key on its chip, and every time you tap, that key produces a fresh signature that the card is genuine and issued by your bank.
+ You already use digital signatures daily: your bank card has a private key on its chip, and every time you tap, that private key produces a fresh signature that the card is genuine and issued by your bank.
The signature is also a grant of authority. By signing, Alice authorizes what the transaction does in her name:
@@ -150,7 +150,7 @@ SOL is the network's **native token**, the asset it understands without any prog
Bob's SOL lives in his own account, but his USDC does not: each token balance lives in a **token account** of its own. The one holding a given wallet's balance of a given token sits at an address anyone can derive from the two. Bob does not need to ask Alice where her USDC lives. Bob's wallet computes where Alice's USDC account must be, and sends the tokens there. The *Tokens* chapter covers token accounts properly.
-So Bob signs a transaction carrying one instruction: the **Token Program**'s `transfer()` handler, moving 25 USDC from his USDC account to Alice's. The instruction says 25,000,000, because USDC's minor unit is a millionth, the same integer rule as lamports. Bob's signature is what authorizes the debit, exactly as Alice's authorized hers: the Token Program checks that the account being debited belongs to Bob, and that Bob signed. Alice's USDC balance rises from 975 to 1,000 with no signature from her.
+So Bob signs a transaction carrying one instruction: the **Token Program**'s `transfer()` handler, moving 25 USDC from his USDC account to Alice's. The instruction says 25,000,000, because USDC's minor unit is a millionth, and like a lamport amount it is an integer of minor units. Bob's signature is what authorizes the debit, exactly as Alice's authorized hers: the Token Program checks that the account being debited belongs to Bob, and that Bob signed. Alice's USDC balance rises from 975 to 1,000 with no signature from her.

@@ -163,11 +163,11 @@ Bob's transfer instruction names every account it touches: Bob's USDC account, A
Two payments toured every building block the rest of the book uses:
- A **key pair**'s public key is an address, and at an address lives an **account**: a user's holds their balance of **SOL**, stored as an integer of **lamports**, its minor unit.
-- **Every transaction is signed.** The signature proves the holder of the private key authorized the transaction, and it is what allows the transaction to debit the signer's accounts and pay its fee: all authority reduces to whose signature a transaction carries.
+- **Every transaction is signed.** The signature proves the holder of the private key authorized the transaction, and it is what allows the transaction to debit the signer's accounts and pay its fee.
- A transaction bundles **instructions**, each addressed to a program's **instruction handler**, and they succeed or fail together: atomicity is a primitive financial software designs with.
- Accounts store data as well as balances: bytes at an address, with a lamport balance and an owner program that users invoke to change them. A deployed program takes accounts of its own, and the state it manages lives in accounts separate again.
- Space is paid for with a **rent** deposit proportional to the account's size, and it is a deposit rather than a fee: closing the account returns it.
-- Addresses come in three kinds: **public keys**, which private keys sign for; **PDAs**, which have no key, where the deriving program "signs" by presenting the seeds; and **built-in addresses**, chosen rather than computed, with neither a key nor seeds.
+- Addresses come in three kinds: **public keys**, which private keys sign for; **PDAs**, which have no private key, where the deriving program "signs" by presenting the seeds; and **built-in addresses**, chosen rather than computed, with neither a private key nor seeds.
- Instructions supply the accounts as arguments; instruction handlers check them.
[^faucets]: The `solana airdrop` command sends devnet SOL, as does the faucet at [faucet.quicknode.com/solana](https://faucet.quicknode.com/solana). Circle's faucet at [faucet.circle.com](https://faucet.circle.com) does the same for devnet USDC.
diff --git a/solana-building-blocks/figures/clock-decision-tree.svg b/solana-building-blocks/figures/clock-decision-tree.svg
index 021e3e23..922ea8f0 100644
--- a/solana-building-blocks/figures/clock-decision-tree.svg
+++ b/solana-building-blocks/figures/clock-decision-tree.svg
@@ -9,7 +9,7 @@
yes
a counter of your own
- neither clock, nothing to bend
+ neither clock, nothing to move
no
@@ -33,7 +33,7 @@
yes
the slot count
- nobody can bend it
+ nobody can move it
no
diff --git a/solana-building-blocks/figures/slot-counter.svg b/solana-building-blocks/figures/slot-counter.svg
index 34787563..ef399e97 100644
--- a/solana-building-blocks/figures/slot-counter.svg
+++ b/solana-building-blocks/figures/slot-counter.svg
@@ -2,7 +2,7 @@
The slot counter
- even steps, only forward, nobody can bend the count
+ even steps, only forward, nobody can move the count
slot n
n+1
n+2
diff --git a/solana-building-blocks/figures/unix-timestamp.svg b/solana-building-blocks/figures/unix-timestamp.svg
index 2cfcd98b..993bd93b 100644
--- a/solana-building-blocks/figures/unix-timestamp.svg
+++ b/solana-building-blocks/figures/unix-timestamp.svg
@@ -7,7 +7,7 @@
- wobble: validator-influenced
+ each leader nudges it
out by a few seconds, whatever the span:
seconds somebody chose
diff --git a/solana-building-blocks/pdas.md b/solana-building-blocks/pdas.md
index 436c62e6..d0b9fd12 100644
--- a/solana-building-blocks/pdas.md
+++ b/solana-building-blocks/pdas.md
@@ -12,7 +12,7 @@ Three things follow from that derivation:
- A token account address either derives from Alice's address and the USDC mint or it does not, so a program handed the wrong address can reject it.
- No private key exists for the result, so tokens can only leave the account via the program that owns the token account.
-These rules apply to any **program derived address**, or PDA. A PDA is an address computed from a program's ID and some chosen seeds, the inputs to the derivation. The derivation deliberately produces an address with no private key: PDA generation tries one extra byte after another, called the **bump**, until the result lands off the curve where public keys live. Programs store the bump they found, so nobody has to search for it again. Only the program whose ID derived the address can "sign" as the PDA, and what "sign" really means gets its own boxout below.
+These three properties hold for any **program derived address**, or PDA. A PDA is an address computed from a program's ID and some chosen seeds, the inputs to the derivation. The derivation deliberately produces an address with no private key: PDA generation tries one extra byte after another, called the **bump**, until the result lands off the curve where public keys live. Programs store the bump they found, so later instructions use the stored bump instead of searching for it again. Only the program whose ID derived the address can "sign" as the PDA, and what "sign" really means gets its own boxout below.
**PDAs are where Solana programs store data.** The Associated Token Program chose its seeds to suit its task: find where a wallet's balance of a given token lives. Every other program chooses its own seeds for its own needs and gets the same three properties. A program's accounts behave like a **key/value store**: the seeds are the key, the PDA they derive is where the value lives, and the data in the account at that PDA is the value. The PDA is the address and the account is what sits at it: a program derives the offer PDA, then writes the offer account. Every program in this book keeps its state this way:
@@ -26,17 +26,17 @@ These rules apply to any **program derived address**, or PDA. A PDA is an addres
> **"Key" here is the database word.** A key/value store's key is an identifier: the thing a value is looked up by. It has nothing to do with the public and private keys of the last three chapters. A PDA has neither of those, and its seeds are what it is looked up by.
-Each of those three jobs again, in the general form every chapter of this book uses:
+Each of those three properties again, in the general form every chapter of this book uses:
## Seeds Make Data Findable
-The market for a token pair, a user's position in it, and the offer with a given ID all live at addresses anyone can compute from the seeds, so exactly one thing exists per key and nobody has to remember where anything is.
+The market for a token pair, a user's position in it, and the offer with a given ID all live at addresses anyone can compute from the seeds, so exactly one thing exists per key. A program finds any of them by recomputing its address from the seeds.
## Seeds Make Relationships Verifiable
-An account that claims to be "the vault for this market" either derives from the market's seeds or it does not. Combined with stored-address checks, derivation is how a program proves that the accounts a caller supplied belong together.
+An account that claims to be "the vault for this market" either derives from the market's seeds or it does not. Combined with stored-address checks, derivation is how a program checks that the accounts a caller supplied belong together.
-## PDA Authority Makes Custody Programmatic
+## Funds Under a PDA's Authority Move Only Through the Program
A vault whose authority is a PDA is a vault whose funds move only along the code paths the program defines. When people say that "the program holds the funds", they mean that the funds are stored in a token account that the program controls, and that moving them means invoking the program's instruction handlers.
diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md
index ed03f47b..b700022c 100644
--- a/solana-building-blocks/time.md
+++ b/solana-building-blocks/time.md
@@ -68,14 +68,14 @@ The network's target slot length is 200 milliseconds at the time of writing.[^sl
A program can get that length three ways:
- **From Anza's documentation, written into your program.**[^anza-docs] This is correct until the network next shortens the slot, and wrong from then on. Keep any window that depends on the slot length in config, where a transaction can change it, which the section on a shortened slot covers.
-- **From the feature gates.** Changes to the network ship as **features**, built into the validator software switched off and turned on across the network at the start of an epoch. Each shortening of the slot ships as one, so a program can check which of them are live and take the shortest, falling back to 400 milliseconds when none are.[^check-slot-time] A new slot time applies from the start of the epoch after its gate activates, not from the moment it does, so check the epoch as well as the gate.
+- **From the feature gates.** Changes to the network ship as **features**, built into the validator software switched off and turned on across the network at the start of an epoch. Each shortening of the slot ships as one, so a program can check which of them are live and take the shortest, falling back to 400 milliseconds when none are.[^check-slot-time] A new slot time applies from the start of the epoch after its feature gate activates, not from the moment it does, so check the epoch as well as the feature gate.
- **From the network itself, in future.** While no account reports the slot time in milliseconds at the time of writing, this is expected to change.[^onchain-params] Check Anza's documentation and the companion repository before writing either technique above, in case such an account now exists.
Avoid the Solana SDK's `DEFAULT_MS_PER_SLOT` - this value is set when the SDK is compiled, so what was true on build day may not reflect the network a program is running on.
-The gates give the length the network aims for, and the network can run slower than that. When slots take longer than the configured length, a 150-slot freshness window covers more wall-clock time than it was sized for, so it accepts older prices.
+The feature gates give the length the network aims for, and the network can run slower than that. When slots take longer than the configured length, a 150-slot freshness window covers more wall-clock time than it was sized for, so it accepts older prices.
-To measure the average the network is actually delivering, take the seconds since `epoch_start_timestamp` in the Clock sysvar and divide by the slots since this epoch began, which `EpochSchedule` supplies. Guard that division: the divisor is zero on an epoch's first slot. That measurement comes from the leader-written timestamp, so it belongs in offchain tooling, not in a handler. A handler that needs wall-clock time should read the timestamp itself, as the lending market's interest does. Multiplying a slot count by a measured slot length carries the timestamp's error and adds the slot count's.
+To measure the average the network is actually delivering, take the seconds since `epoch_start_timestamp` in the Clock sysvar and divide by the slots since this epoch began, which `EpochSchedule` supplies. Check the divisor before that division: it is zero on an epoch's first slot. That measurement comes from the leader-written timestamp, so it belongs in offchain tooling, not in a handler. A handler that needs wall-clock time should read the timestamp itself, as the lending market's interest does. Multiplying a slot count by a measured slot length carries the timestamp's error and adds the slot count's.
> **One conversion covers one slot time.** Converting only works over a period where the slot time in milliseconds was constant. If the slot time changed during the period being measured, multiplying by the current figure understates the time that passed, since slots in the past were longer. Converting over a period where the slot time changed means splitting it at each change, which always falls on an epoch boundary, and converting each piece at the slot time the network used then. A slot count that reaches back across the reduction to 200 milliseconds needs that split, while one that starts after it does not. At the time of writing, no reduction below 200 milliseconds is planned.
@@ -125,9 +125,9 @@ Both handlers compare that one count against the same duration:
- `contribute()` requires it to be *less than* the duration.
- `refund()` requires it to be *at least* the duration.
-Because both handlers compare the same expression, there is no gap and no overlap between "you may still contribute" and "you may now refund". At the moment the count reaches the duration, contributions turn off and refunds turn on, by arithmetic rather than by a second stored flag anybody has to keep in step.
+Because both handlers compare the same expression, there is no gap and no overlap between "you may still contribute" and "you may now refund". At the moment the count reaches the duration, `contribute()` starts failing with `FundraiserEnded` and `refund()` stops failing with `FundraiserNotEnded`, with no second stored flag that has to be kept in step with the count.
-The trap is what "seven days" then means: the window ends at the first tick of day seven, not at the end of the seventh day.
+The trap is what "seven days" means to the people using it. The program closes the window exactly 168 hours after `time_started`, at the same time of day it opened, not at midnight at the end of the seventh calendar day.

@@ -143,9 +143,9 @@ A halt is rare: at the time of writing, block production has not stopped for yea
No block is produced during a halt, so the slot count does not move. Wall-clock time does. The first block after a restart carries a slot roughly where the last one left off. Its timestamp cannot jump the hours the halt took either, because the network rejects a block whose time runs further ahead than the slots since its parent allow. The timestamp trails real time after a restart and catches up over the blocks that follow. That gap has three consequences:
-- **Accrual by timestamp charges the halt, a little late.** The lending market's interest, the perpetual futures venue's funding, and the vault's fee all count seconds, so a nine-hour halt accrues nine hours. It arrives over the catch-up rather than in the first block. The loans were outstanding and the suppliers' money was locked for those nine hours, so charging for them is accurate.
+- **Accrual by timestamp charges the halt, a little late.** The lending market's interest, the perpetual futures venue's funding, and the vault's fee all count seconds, so a nine-hour halt accrues nine hours. It arrives over the catch-up rather than in the first block. The loans were outstanding and the suppliers' deposits were locked for those nine hours, so charging for them is accurate.
- **Neither kind of freshness window refuses the old price on its own.** The lending market's 25-slot window sees a price stamped one slot ago and would accept a number nine hours old. The vault's sixty-second window does no better, because the timestamp it reads is trailing, so a price published just before the halt looks seconds old. The damage is largest on the perpetual futures venue, where leverage multiplies a bad price across every open position.
-- **A deadline can pass with nobody able to act on it.** The fundraiser's day count runs through a halt like any other nine hours. A contributor with an hour left before the halt gets less than an hour after it, because the timestamp runs fast while it catches up. That program survives it because its refund path has no deadline; a design whose only exit was a narrow window would have stranded them.
+- **A deadline can pass with nobody able to act on it.** The fundraiser's day count runs through a halt like any other nine hours. A contributor with an hour left before the halt gets less than an hour after it, because the timestamp runs fast while it catches up. The fundraiser survives it because `refund()` has no closing deadline, so contributors can still get their money back after the halt. A design that allowed refunds only during a narrow window would have left them unable to.
Every freshness window needs one more check. The `LastRestartSlot` sysvar[^last-restart-slot] carries the slot of the most recent cluster restart, or zero if there has never been one, and a price from before that restart is stale however few slots have passed since. The lending market, the prop AMM, the perpetual futures venue, and the managed fund all make this check in their oracle readers: any price stamped at or before the restart slot is rejected with `PricePredatesRestart`, and the market pauses valuation until the publisher posts again. Make it in any program that checks a price's age, in slots or in seconds.
@@ -155,7 +155,7 @@ The order book comes through a halt untouched, because it reads neither clock: q
The network has shortened the slot before, most recently in stages from 400 milliseconds to 200.[^slot-times] The change is rare and happens in one step: the number holds for years, then moves once.
-A freshness window sized in slots gets stricter. A 150-slot window covers less wall-clock time than it did, so it refuses prices it used to accept. That costs availability and never safety, and it announces itself the first time a price is refused. The defense is to keep the window where a transaction can reach it, so the change needs a configuration transaction rather than a redeploy.
+A freshness window sized in slots gets stricter. A 150-slot window covers less wall-clock time than it did, so it refuses prices it used to accept. That costs availability and never safety, and it announces itself the first time a price is refused. The defense is to store the window in an account a transaction can update, so the change needs a configuration transaction rather than a redeploy.
A rate counted in seconds is untouched. The lending market's interest and the perpetual futures venue's funding charge the same per hour at any slot length, with nothing to retune. A rate counted in slots gives no such warning when the slot shortens. An annual rate divided by a fixed slots-per-year number assumes a slot length, so halving the slot doubles what a borrower pays in a day while every line of the program stays as it was.
@@ -168,9 +168,9 @@ The table lists every program in Part II that touches time, and the clock each o
| Program | What it times | Its answer | Why |
| --- | --- | --- | --- |
| fundraiser | its funding deadline | whole days from one stored timestamp | a week of slots is [PLACEHOLDER: measure hours of drift per week after 200 ms slots and Alpenglow go live] out |
-| lending market | interest accrual, and price freshness | timestamps for interest, slots for freshness | seconds against a year are nothing, and a stale price is the attack |
+| lending market | interest accrual, and price freshness | timestamps for interest, slots for freshness | seconds against a year are nothing, and a stale price can be exploited |
| perpetual futures | funding accrual, and price freshness | timestamps for funding, slots for freshness | the same, and leverage multiplies a stale price |
-| prop AMM | price freshness | slots | a stale price is the attack |
+| prop AMM | price freshness | slots | a stale price can be exploited |
| managed fund | an annual fee, and price freshness | timestamps for both | seconds against a year are nothing, and the feed stamps seconds |
| order book | which order came first | a sequence number, neither clock | neither clock is fine enough to order by |
@@ -179,10 +179,10 @@ The table lists every program in Part II that touches time, and the clock each o
- Solana has two clocks. The slot count only goes up and nobody can move it, though what it comes to in seconds varies a little and stops in a halt. The Unix timestamp is wall-clock time written by a leader, out by a few seconds somebody chose, however long the span.
- Count slots for price freshness and for anything an adversary running a leader could profit from by shifting the time a few seconds. Use the wall clock for a rate or a deadline quoted to a person, with a margin at any deadline. Use a counter of your own for order of arrival, and whatever unit the publisher stamped for somebody else's number.
- To accrue interest or funding without touching every account, store each account's principal and one shared accumulation factor that advances by elapsed seconds. A current balance is the principal multiplied by the factor.
-- A price needs a freshness window, each consumer sets its own, and the unit follows the source: slots where the program chooses, seconds where the feed published seconds.
+- Give every price a freshness window, set by each consumer, in the unit the source uses: slots where the program chooses, seconds where the feed published seconds.
- A slot count converts to wall-clock time only at the slot length in force, and only across a span where that length held. Read that length from the slot-time feature gates or measure it against the epoch, and better still hold it as a figure you can update without a redeploy. Anza's documentation carries the current figure, and the companion repository the current way to get it.
-- A shortened slot is rare and arrives all at once. Every slot-counted window tightens, so keep windows where a transaction can retune them. A rate counted in seconds is untouched, while one counted in slots accrues faster without saying so.
-- A deadline counted in whole days floors, so both sides of it must be computed from the same expression, and the window ends at the first tick of the last day. Say which edge you meant when you write the terms down.
+- The network occasionally shortens its slot time, as it did when slots went from 400 milliseconds to 200, in stages. Each change takes effect everywhere at the start of an epoch, with no transition. Every slot-counted window then covers less time, so store windows in an account a transaction can update. A rate counted in seconds is untouched, while one counted in slots accrues faster without saying so.
+- A deadline counted in whole days floors, so compute both sides of it from the same expression. The window closes exactly that many 24-hour periods after it opened, at the same time of day, so say so when you write the terms down.
- For order of arrival, use a number of your own: the order book gives each resting order the next number from a counter it keeps, and keeps its timestamp for display only.
- Solana halts rather than run two conflicting histories. During a halt slots pause and wall-clock time keeps running, but the timestamp cannot jump, so it trails after a restart and catches up. Accrual by timestamp charges the halt during the catch-up, deadlines expire with nobody able to act on them, and a price that is hours old still looks fresh.
- Pair every freshness window, in slots or in seconds, with a `LastRestartSlot` check, which rejects any price stamped at or before the last restart.
diff --git a/solana-building-blocks/tokens.md b/solana-building-blocks/tokens.md
index 31ef4fe6..c20bdf8c 100644
--- a/solana-building-blocks/tokens.md
+++ b/solana-building-blocks/tokens.md
@@ -6,7 +6,7 @@ Assets on Solana are **tokens**, and every token is defined by a mint account an
- equities like NVDAx, SPCXx, and TSLAx[^tokenized-equity-volume]
- the share tokens the book's own programs mint
-So is every other asset on Solana.
+So is every other asset on Solana.[^tokens-xyz]
## Every Distinct Token Has Its Own Mint
@@ -18,7 +18,7 @@ Each kind of token is defined by its own **mint account**.
A mint is not itself a token: it is the account a token comes from, and no amount of anything is held in the mint.
-Two tokens are the same asset if they come from the same mint address, and different assets if they do not: anyone can create a mint whose name and symbol say "USDC", so the address is the identity and the label is decoration. When this book says "USDC" or "NVDAx", the thing that identifies that asset onchain is a mint account. Instruction handlers are given account addresses as arguments, and must check that any mint account address matches the expected value, so that the asset is the one it claims to be.
+Two tokens are the same asset if they come from the same mint address, and different assets if they do not. Anyone can create a mint whose name and symbol say "USDC", so the mint address identifies the asset and the name and symbol do not. When this book says "USDC" or "NVDAx", the thing that identifies that asset onchain is a mint account. Instruction handlers are given account addresses as arguments, and must check that any mint account address matches the expected value, so that the asset is the one it claims to be.
## A Mint Is a Factory, and the Mint Authority Is Its Boss
@@ -26,7 +26,7 @@ A mint does not just describe a type of token; it manufactures it. New tokens co
As well as the current total supply, the mint account also stores the token's decimals (how many minor units are in a major unit), and its authorities.
-The **mint authority** is the boss of that factory, and who holds the role is the first question to ask about any token. It can be a person's wallet (they can mint at will: fine for a test token, alarming for money), a multisig (minting takes a quorum), or a PDA. This book's programs use a PDA: the lending market's share mint and the fund's share mint are run by program-owned addresses, so issuance follows code, not a person. The authority can also be handed over, or renounced entirely by setting it to none, which fixes the supply forever. Many tokens explicitly advertise a fixed supply, and disabling the `mint_authority` is how it is enforced.
+The **mint authority** is the boss of that factory, and who holds the role is the first question to ask about any token. It can be a person's wallet (they can mint at will: fine for a test token, alarming for a token people rely on), a multisig (minting takes a quorum), or a PDA. This book's programs use a PDA: the lending market's share mint and the fund's share mint are run by program-owned addresses, so new shares are minted only by the program's handlers, never by a person. The authority can also be handed over, or renounced entirely by setting it to none, which fixes the supply forever. Many tokens explicitly advertise a fixed supply, and disabling the `mint_authority` is how it is enforced.

@@ -60,7 +60,7 @@ The mint's other authority is the **freeze authority**, which can freeze and tha
## Two Token Programs: Classic and Token Extensions
-Solana has two token programs. The **Classic Token Program** is the original, and most tokens in circulation use it. The **Token Extensions Program** is its successor: the same core machinery, plus opt-in, per-mint features. Well-written programs support both through a common interface (like Anchor's `token_interface`), and only require that every account in one transaction uses the same token program. Every program in this book works that way.
+Solana has two token programs. The **Classic Token Program** is the original, and most tokens in circulation use it. The **Token Extensions Program** is its successor: the same core instructions and accounts, plus opt-in, per-mint features. Well-written programs support both through a common interface (like Anchor's `token_interface`), and only require that every account in one transaction uses the same token program. Every program in this book works that way.
The table lists the extensions that matter for financial products:
@@ -68,7 +68,7 @@ The table lists the extensions that matter for financial products:
| --- | --- | --- |
| **transfer fees** | A cut is withheld on every transfer, for the issuer to collect | Send 100 and 99 arrives: "I transferred X" no longer delivers X |
| **transfer hooks** | A program of the issuer's choosing runs on every transfer | Enables allowlists, and means a transfer can execute foreign code (the ordering section of the *Adversary* chapter takes this seriously) |
-| **confidential transfers** | Amounts are hidden onchain | Balances are visible only to the parties and an auditor key |
+| **confidential transfers** | Amounts are hidden onchain | Balances are visible only to the parties and whoever holds the auditor's private key |
| **permanent delegate** | An authority can move any holder's tokens | A seizure power some regulated assets require |
| **interest-bearing** | A rate is stamped on the mint | Balances display accrual without the supply changing |
| **non-transferable** | Tokens are bound to their holder | For credentials and vesting, not payments |
@@ -79,8 +79,8 @@ Before building on a token you did not issue, check which extensions it uses: So
## What You Learnt
-- Every distinct token has its own **mint account**, and that account's address is the token's identity: the label is decoration. A mint defines a token; it is not one.
-- The **mint authority** and **freeze authority** are the first questions to ask about any token: who can mint, and who can freeze.
+- Every distinct token has its own **mint account**, and that account's address identifies the token, whatever name and symbol it shows. A mint defines a token; it is not one.
+- Ask about the **mint authority** and **freeze authority** of any token first: who can mint, and who can freeze.
- **Token accounts hold tokens**: ATAs are the derivable canonical ones for an address, and program vaults can live anywhere the program chose.
- **Decimals** define the minor unit: read them from the mint, never assume them, and transfer with `transfer_checked()`.
- Solana has two token programs, the **Classic Token Program** and the **Token Extensions Program**. Some extensions take a fee from every transfer, run the issuer's program on every transfer, or let the issuer move any holder's tokens: before building on a token, check in Solana Explorer which extensions it uses.
@@ -89,3 +89,4 @@ Before building on a token you did not issue, check which extensions it uses: So
[^usdc-address]: Circle, the USDC issuer, lists its mint addresses at [developers.circle.com/stablecoins/usdc-contract-addresses](https://developers.circle.com/stablecoins/usdc-contract-addresses).
[^nvdax-address]: xStocks, the NVDAx issuer, lists its tokenized equities at [xstocks.fi/us/products](https://xstocks.fi/us/products).
[^tokenized-equity-volume]: Tokens tracking Tesla and Nvidia traded $631M and $380M on Solana in the twelve months to August 18, 2026, among the eight most traded tokenized equities on the chain, behind the S&P 500 token's $2.0B. Source: Allium, [allium.so/solana](https://allium.so/solana).
+[^tokens-xyz]: [tokens.xyz](https://tokens.xyz) lists which assets are available as Solana tokens.