Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f94b7a7
Replace coined labels with the checks, handlers and fields they stand…
Sep 29, 2026
7c34bee
Perps: LPs earn traders' losses and funding, not fees
Sep 29, 2026
5386470
Glossary: a degenerate offer passes the usual checks but is worthless
Sep 29, 2026
4c526f2
Replace "money" and "guard" with what they stand for; define K in the…
Sep 30, 2026
d6643fe
Call Kani harnesses model checks, not proofs; define asset and token
Sep 30, 2026
98631f2
Keep the idioms "put money on" and "get their money back"
Sep 30, 2026
d121fab
Tokens: point to tokens.xyz for which assets are available as Solana …
Sep 30, 2026
5d014f6
Keep "money" where it is everyday speech about people
Sep 30, 2026
9fd0e61
Betting market: Draft is when outcomes can change, and opening fixes …
Sep 30, 2026
c45a600
Betting market: say exactly when close_losing_bet() works
Sep 30, 2026
15ca65b
Glossary: open source means a license that meets the Open Source Defi…
Sep 30, 2026
1594e55
Say a missing handler is hypothetical where the program has it
Sep 30, 2026
fbc67c2
Escrow: the maker's offered tokens would be stranded in the program
Sep 30, 2026
88e3c2d
Options: the market's vault holds the writer's shares, not the option
Sep 30, 2026
064aade
Fundraiser: contributions come back unless the full target amount is …
Sep 30, 2026
be88002
Name the character where a pronoun would read as a role's
Sep 30, 2026
cac3b71
Say private key or public key, never a bare key
Sep 30, 2026
2662225
Managed fund: give the slippage cap its own paragraph and its own reason
Sep 30, 2026
55fa12e
State what makes it so, not that nothing needs to happen
Sep 30, 2026
4169107
Set run-in lists as bullet lists
Sep 30, 2026
be6a488
Managed fund: the manager is included in the threat model; Victor and…
Sep 30, 2026
38d8871
Stop stating a part as the whole
Sep 30, 2026
87e7868
Managed fund: point to the Fundraiser footnote instead of repeating i…
Sep 30, 2026
de3215a
Say what happens to the tokens instead of calling it a lock
Sep 30, 2026
e8a4e9d
Perps: collect_fees() is not Maria's only signed handler
Sep 30, 2026
13b89bf
Follow the companion's program_fees and "moves into vault" renames
Sep 30, 2026
617dde0
Merge origin/main into claude/plain-wording-sweep
Sep 30, 2026
f2fed92
Replace "machinery" with the program or software it stood for, and li…
Sep 30, 2026
b5e22ff
Conclusion: say "money" where the product ideas talk about the offcha…
Sep 30, 2026
6f0e648
Conclusion: keep "capital to invest", and send the friends for a run
Sep 30, 2026
0967db0
Say "if" instead of "the day" for a condition
Sep 30, 2026
e333c50
Glossary: link the RWA entry to asset and token
Sep 30, 2026
73cf51c
Glossary: RWA entry recommends naming the asset class instead
Sep 30, 2026
16edd67
Glossary: add crypto, a wrapper for trading asset classes rather than…
Sep 30, 2026
b575c65
Glossary: crypto entry ends with the token sentence
Sep 30, 2026
fa05200
Offchain Truth: say the stopped-publication question belongs in the d…
Sep 30, 2026
0ffcf44
Glossary: add Solana, which is neither an asset nor an organization
Sep 30, 2026
14a9378
Glossary: Solana entry names no organizations
Sep 30, 2026
cb0a550
Offchain Truth: say plainly that an oracle's publication must be checked
Sep 30, 2026
a433fba
Order book: simplify the eviction sentences
Sep 30, 2026
e92c3f6
Offchain Truth: "an oracle update"
Sep 30, 2026
60ad6ff
State machines: tell the reader to make the last payout close everything
Sep 30, 2026
1b17a8e
Write rules as instructions in the rules lists and summaries
Sep 30, 2026
6e75ee5
Say "values" for what a test or model checker tries, and note why Kan…
Sep 30, 2026
878dbe9
Clusters table: devnet is where developers deploy in-progress software
Sep 30, 2026
fe7ec6a
Clusters: other clusters try out a particular proposed network feature
Sep 30, 2026
b259aa8
Time: say what a shortened slot does, in the summary
Sep 30, 2026
d94cb14
Time: name the 400 to 200 millisecond change in the summary
Sep 30, 2026
e5c44e2
Time: a seven-day window closes 168 hours after it opened, not at mid…
Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions WRITING-PLAN.md

Large diffs are not rendered by default.

73 changes: 52 additions & 21 deletions appendices/glossary.md

Large diffs are not rendered by default.

44 changes: 22 additions & 22 deletions building-financial-products-in-solana/betting-market.md

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions building-financial-products-in-solana/constant-product-amm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Loading
Loading