From 0a37a0bbde1cd717a328719a519bd637b6bba8c7 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Mon, 28 Sep 2026 15:13:05 +0000 Subject: [PATCH 1/2] Rename the Vault Strategy chapter to The Managed Fund The product the chapter builds is a manager-run fund with shares priced at NAV: in finance terms a managed fund, the onchain equivalent of a mutual fund. It is not an index fund, which the chapter and the table of contents called it, because its manager chooses the weights. "Vault" already means a program-owned token account everywhere else in the book. - The chapter becomes managed-fund.md, titled The Managed Fund, and its figures become fund-*.svg with their labels renamed. - Identifiers follow the companion repository's rename: finance/managed-fund, initialize_fund(), FundNotFullyAllocated, the "fund" seed. - A boxout at the top of the chapter says the product is also called a vault strategy, why the book does not use that name, and how it differs from an index fund. The glossary's strategy entry becomes managed fund, and a new vault strategy entry points to it. - Cross-references, the table of contents, the Amazon description, the writing plan and the linter's chapter-title list follow. Claude-Session: https://claude.ai/code/session_01JTaqedahgCHQbzXPcHDPok --- AMAZON-DESCRIPTION.md | 2 +- TABLE-OF-CONTENTS.md | 2 +- WRITING-PLAN.md | 6 +-- appendices/glossary.md | 20 ++++--- .../constant-product-amm.md | 2 +- ...t-account-map.svg => fund-account-map.svg} | 30 +++++------ ...lt-create-strategy.svg => fund-create.svg} | 32 ++++++------ .../{vault-deposit.svg => fund-deposit.svg} | 28 +++++----- ...fee-dilution.svg => fund-fee-dilution.svg} | 0 ...vault-rebalance.svg => fund-rebalance.svg} | 26 +++++----- .../{vault-registry.svg => fund-registry.svg} | 4 +- ...-in-kind.svg => fund-withdraw-in-kind.svg} | 28 +++++----- .../lending.md | 2 +- .../{vault-strategy.md => managed-fund.md} | 52 +++++++++---------- .../perpetual-futures.md | 2 +- conclusion/where-to-go-from-here.md | 8 +-- .../offchain-truth.md | 6 +-- .../operating-financial-software.md | 4 +- .../the-adversary.md | 10 ++-- .../the-arithmetic-of-money.md | 2 +- scripts/check-figure-bands.mjs | 2 +- scripts/check-figure-geometry.mjs | 2 +- scripts/lint-manuscript.mjs | 2 +- solana-building-blocks/time.md | 8 +-- solana-building-blocks/tokens.md | 2 +- 25 files changed, 143 insertions(+), 139 deletions(-) rename building-financial-products-in-solana/figures/{vault-account-map.svg => fund-account-map.svg} (97%) rename building-financial-products-in-solana/figures/{vault-create-strategy.svg => fund-create.svg} (96%) rename building-financial-products-in-solana/figures/{vault-deposit.svg => fund-deposit.svg} (97%) rename building-financial-products-in-solana/figures/{vault-fee-dilution.svg => fund-fee-dilution.svg} (100%) rename building-financial-products-in-solana/figures/{vault-rebalance.svg => fund-rebalance.svg} (97%) rename building-financial-products-in-solana/figures/{vault-registry.svg => fund-registry.svg} (99%) rename building-financial-products-in-solana/figures/{vault-withdraw-in-kind.svg => fund-withdraw-in-kind.svg} (97%) rename building-financial-products-in-solana/{vault-strategy.md => managed-fund.md} (65%) diff --git a/AMAZON-DESCRIPTION.md b/AMAZON-DESCRIPTION.md index 2ae93263..946c710e 100644 --- a/AMAZON-DESCRIPTION.md +++ b/AMAZON-DESCRIPTION.md @@ -10,7 +10,7 @@ Written for engineers shipping software that holds other people's money, and for **Part I** builds the fundamentals: Solana's accounts, tokens, PDAs, and time, then financial software as a state machine, the arithmetic of money, offchain truth, the adversary, and operating a program in production. Exploit classes are taught through the incidents that made them famous, among them the $2.6B SPL token-lending rounding bug, Cashio ($52M), Wormhole ($325M), and Mango Markets ($116M). -**Part II** walks ten complete, tested programs end to end: an escrow, an all-or-nothing token fundraiser, a parimutuel betting market, an order book exchange with a critbit matching engine, a constant-product AMM, an oracle-priced proprietary AMM, a lending and borrowing market with per-second interest and a liquidation engine, an onchain index fund, a fully collateralized options venue, and a perpetual futures venue with funding, margin, and pre-funded solvency. Every chapter gives the account model, the instruction handlers walked as a story, the math in minor units with its rounding directions, the invariants, the failure modes, the tests that catch each one, and the incentives of everyone involved, operator and user alike. Account maps, state machines, and value flows are drawn in one visual language across all ten, so the programs can be read against each other. They have not been explained side by side in one place before. +**Part II** walks ten complete, tested programs end to end: an escrow, an all-or-nothing token fundraiser, a parimutuel betting market, an order book exchange with a critbit matching engine, a constant-product AMM, an oracle-priced proprietary AMM, a lending and borrowing market with per-second interest and a liquidation engine, a managed fund (what Solana platforms often call a vault strategy), a fully collateralized options venue, and a perpetual futures venue with funding, margin, and pre-funded solvency. Every chapter gives the account model, the instruction handlers walked as a story, the math in minor units with its rounding directions, the invariants, the failure modes, the tests that catch each one, and the incentives of everyone involved, operator and user alike. Account maps, state machines, and value flows are drawn in one visual language across all ten, so the programs can be read against each other. They have not been explained side by side in one place before. In the tradition of *TCP/IP Illustrated*, the book teaches the layer that does not change. Frameworks churn. The account model has been stable since Solana launched, the arithmetic of money will not change, and neither will the desire of adversaries to take your assets. diff --git a/TABLE-OF-CONTENTS.md b/TABLE-OF-CONTENTS.md index 0b95d8cb..9cff0410 100644 --- a/TABLE-OF-CONTENTS.md +++ b/TABLE-OF-CONTENTS.md @@ -34,7 +34,7 @@ Each chapter explains one complete, tested program from the [companion repositor - [Chapter 15: The Constant-Product AMM](building-financial-products-in-solana/constant-product-amm.md) - instant swaps priced by a formula - [Chapter 16: The Prop AMM](building-financial-products-in-solana/prop-amm.md) - a market maker quoting its own capital at oracle prices - [Chapter 17: Lending and Borrowing](building-financial-products-in-solana/lending.md) - deposits, collateral, interest, liquidation, and cranks -- [Chapter 18: The Vault Strategy](building-financial-products-in-solana/vault-strategy.md) - an onchain index fund +- [Chapter 18: The Managed Fund](building-financial-products-in-solana/managed-fund.md) - an onchain mutual fund, also called a vault strategy - [Chapter 19: Options](building-financial-products-in-solana/options.md) - calls and puts, fully collateralized and physically settled - [Chapter 20: Perpetual Futures](building-financial-products-in-solana/perpetual-futures.md) - leveraged bets on a price, and the last of the ten programs diff --git a/WRITING-PLAN.md b/WRITING-PLAN.md index 811b71b9..79eccd55 100644 --- a/WRITING-PLAN.md +++ b/WRITING-PLAN.md @@ -57,7 +57,7 @@ Each incident is researched from primary sources and placed inside the chapter n - SPL token-lending rounding (about $2.6B at risk): the rounding section of The Arithmetic of Money - Cashio ($48M kept after refunds, unvalidated account chain): the validation section of The Adversary - Wormhole ($326M, bypassed signature verification): the validation section of The Adversary -- First-depositor inflation incidents (the ERC-4626 class): named incidents are the Hundred and Sonne stories below (their setup is the inflation attack); the empty-pool section of The Adversary teaches the class, The Constant-Product AMM chapter teaches the locked-minimum defense without an incident box, and The Vault Strategy chapter teaches ignoring donations +- First-depositor inflation incidents (the ERC-4626 class): named incidents are the Hundred and Sonne stories below (their setup is the inflation attack); the empty-pool section of The Adversary teaches the class, The Constant-Product AMM chapter teaches the locked-minimum defense without an incident box, and The Managed Fund chapter teaches ignoring donations - Mango Markets ($116M): the price-manipulation section of The Adversary - The Hundred and Sonne precision drains: the precision-exploits section of The Adversary @@ -74,7 +74,7 @@ Steps currently missing their per-step diagram, to be drawn in the visual pass: - Lending and Borrowing: Carol's short; Bob's repayment and collateral withdrawal. - Perpetual Futures: Alice's `close_position` payout. - The Prop AMM: Maria's `deposit_inventory` and `withdraw_inventory`. -- The Vault Strategy: Bob's deposit at the new share price. +- The Managed Fund: Bob's deposit at the new share price. ## Reviewers, Feedback, and Editions @@ -85,7 +85,7 @@ Steps currently missing their per-step diagram, to be drawn in the visual pass: ## Milestones - **Voice samples first** (drafted): the Introduction, Escrow, and The Order Book Exchange lock voice, depth, and template. -- The remaining program chapters, one per example, in table-of-contents order (all ten drafted: Escrow, Token Fundraiser, The Betting Market, The Order Book Exchange, The Constant-Product AMM, The Prop AMM, Lending and Borrowing, The Vault Strategy, Options, Perpetual Futures). They run as three groups: custody and pooling, then market making, then credit and derivatives. A chapter's place is set by what it assumes the reader already has, so The Betting Market sits third, where its only prerequisite is Escrow, and Options sits just before Perpetual Futures, because a fully collateralized derivative is the case the leveraged one relaxes. +- The remaining program chapters, one per example, in table-of-contents order (all ten drafted: Escrow, Token Fundraiser, The Betting Market, The Order Book Exchange, The Constant-Product AMM, The Prop AMM, Lending and Borrowing, The Managed Fund, Options, Perpetual Futures). They run as three groups: custody and pooling, then market making, then credit and derivatives. A chapter's place is set by what it assumes the reader already has, so The Betting Market sits third, where its only prerequisite is Escrow, and Options sits just before Perpetual Futures, because a fully collateralized derivative is the case the leveraged one relaxes. - The two Fundamentals sections (Solana Building Blocks; Financial Programming Concepts in Solana), with technical reviewers engaged per chapter as each completes. - The remaining appendices. The Glossary is drafted. - The placeholder front-matter pages last. diff --git a/appendices/glossary.md b/appendices/glossary.md index 4c5fd703..712a47f0 100644 --- a/appendices/glossary.md +++ b/appendices/glossary.md @@ -159,7 +159,7 @@ A binary tree keyed on the first bit where entries differ. Used for order books ### curator -In a fund, the authority that decides which assets may be held at all and which price feed prices each one, kept separate from the [manager](#manager) who picks the allocation. The split is what stops a manager listing a token they minted themselves, or pairing a real asset with a feed they control. See the *Vault Strategy* chapter. +In a fund, the authority that decides which assets may be held at all and which price feed prices each one, kept separate from the [manager](#manager) who picks the allocation. The split is what stops a manager listing a token they minted themselves, or pairing a real asset with a feed they control. See the *Managed Fund* chapter. ### custody @@ -377,9 +377,13 @@ The human-scale denomination of an asset: the dollar, the SOL, the whole share. The maker posted the resting order; the taker crossed the spread to hit it. Fills execute at the maker's posted price, and fee schedules usually distinguish the two roles. +### 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. + ### manager -In a fund, the authority that sets the allocation and earns the management fee, and that can do nothing else: no instruction handler gives them a path to the assets. What a careless manager can still cost depositors is churn, rebalancing that pays [slippage](#slippage) for nothing. See [curator](#curator) and the *Vault Strategy* chapter. +In a fund, the authority that sets the allocation and earns the management fee, and that can do nothing else: no instruction handler gives them a path to the assets. What a careless manager can still cost depositors is churn, rebalancing that pays [slippage](#slippage) for nothing. See [curator](#curator) and the *Managed Fund* chapter. ### mantissa @@ -523,7 +527,7 @@ The thing a market states its prices in (USDC), and the unit a **quote amount** ### rebalance -Trading a fund back toward its target weights after prices have moved it away. In this book's fund every leg routes through the same swap program at oracle-anchored minimums, so a rebalance cannot be dressed up as a withdrawal. See the *Vault Strategy* chapter. +Trading a fund back toward its target weights after prices have moved it away. In this book's fund every leg routes through the same swap program at oracle-anchored minimums, so a rebalance cannot be dressed up as a withdrawal. See the *Managed Fund* chapter. ### receipt @@ -617,10 +621,6 @@ A token designed to hold a fixed value, usually one dollar. The quote asset of n A technique for storing bulk data offchain with onchain proofs, used for things like very large NFT collections. It is the reason "nearly all", rather than "all", data on Solana lives in accounts; none of the financial state in this book uses it. -### strategy - -The fund-shaped product: 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). Distinct from a [vault](#vault), which in this book is only ever the token account the money sits in. See the *Vault Strategy* chapter. - ### strike The price at which an [option](#option) trades if its holder exercises. The *Options* chapter stores it as an amount of the quote token per contract rather than as a price, so every settlement is a multiplication with no division to round. @@ -675,7 +675,11 @@ A computer running the Solana software: it executes transactions, votes on block ### vault -A token account whose authority is a program's PDA, where a program holds funds mid-lifecycle. In this book, always the token account, never the whole product; the fund-shaped product is a [strategy](#strategy). +A token account whose authority is a program's PDA, where a program holds funds mid-lifecycle. In this book, always the token account, never the whole product; the fund-shaped product is a [managed fund](#managed-fund). + +### vault strategy + +Another name for a [managed fund](#managed-fund), used by several Solana platforms, and the name this book's managed-fund example had before it took the finance term. This book avoids it because a [vault](#vault) here is a token account, not a product. See the *Managed Fund* chapter. ### WAD diff --git a/building-financial-products-in-solana/constant-product-amm.md b/building-financial-products-in-solana/constant-product-amm.md index 0260fc1d..fb5cb62d 100644 --- a/building-financial-products-in-solana/constant-product-amm.md +++ b/building-financial-products-in-solana/constant-product-amm.md @@ -140,4 +140,4 @@ Six concepts came with the curve: - **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. -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 *Vault Strategy* 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 other people's money, and the question there is what the manager is allowed to do with it. diff --git a/building-financial-products-in-solana/figures/vault-account-map.svg b/building-financial-products-in-solana/figures/fund-account-map.svg similarity index 97% rename from building-financial-products-in-solana/figures/vault-account-map.svg rename to building-financial-products-in-solana/figures/fund-account-map.svg index a99565e6..fd1653f7 100644 --- a/building-financial-products-in-solana/figures/vault-account-map.svg +++ b/building-financial-products-in-solana/figures/fund-account-map.svg @@ -1,5 +1,5 @@ - Every account the strategy holds + Every account the fund holds Solana Devnet @@ -137,11 +137,11 @@ owner: Pyth Receiver - vault-strategy program + managed-fund program initialize_registry() approve_asset() - initialize_strategy() + initialize_fund() add_asset() set_weight() deposit() @@ -151,10 +151,10 @@ - "STRATEGY" + INDEX + "FUND" + INDEX - strategy + fund manager: Maria registry: Victor's @@ -166,7 +166,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 0 @@ -177,7 +177,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 1 @@ -188,47 +188,47 @@ - "SHARE_MINT" + STRATEGY + "SHARE_MINT" + FUND share mint supply: 1,350 shares - mint authority: the strategy PDA + mint authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + USDC MINT USDC vault 0 USDC - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + TSLAX MINT TSLAx vault ≈2.30 TSLAx - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + NVDAX MINT NVDAx vault ≈4.32 NVDAx - authority: the strategy PDA + authority: the fund PDA diff --git a/building-financial-products-in-solana/figures/vault-create-strategy.svg b/building-financial-products-in-solana/figures/fund-create.svg similarity index 96% rename from building-financial-products-in-solana/figures/vault-create-strategy.svg rename to building-financial-products-in-solana/figures/fund-create.svg index 1c9261b7..1a4c70e7 100644 --- a/building-financial-products-in-solana/figures/vault-create-strategy.svg +++ b/building-financial-products-in-solana/figures/fund-create.svg @@ -1,5 +1,5 @@ - Maria creates the strategy + Maria creates the fund Solana Devnet @@ -102,11 +102,11 @@ - vault-strategy program + managed-fund program initialize_registry() approve_asset() - initialize_strategy() + initialize_fund() add_asset() ×2 set_weight() deposit() @@ -116,10 +116,10 @@ - "STRATEGY" + INDEX + "FUND" + INDEX - strategy + fund manager: Maria NEW @@ -132,7 +132,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 0 @@ -144,7 +144,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 1 @@ -156,19 +156,19 @@ - "SHARE_MINT" + STRATEGY + "SHARE_MINT" + FUND share mint supply: 0 shares NEW - mint authority: the strategy PDA + mint authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + USDC MINT @@ -176,11 +176,11 @@ 0 USDC NEW - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + TSLAX MINT @@ -188,11 +188,11 @@ 0 TSLAx NEW - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + NVDAX MINT @@ -200,7 +200,7 @@ 0 NVDAx NEW - authority: the strategy PDA + authority: the fund PDA @@ -226,7 +226,7 @@ Maria signs, and fronts the rent for - the strategy, its share mint and three vaults; + the fund, its share mint and three vaults; no tokens move until an investor deposits not drawn: the TSLAx, NVDAx and USDC mints, named only as seeds. diff --git a/building-financial-products-in-solana/figures/vault-deposit.svg b/building-financial-products-in-solana/figures/fund-deposit.svg similarity index 97% rename from building-financial-products-in-solana/figures/vault-deposit.svg rename to building-financial-products-in-solana/figures/fund-deposit.svg index a79a2ce7..17267fbb 100644 --- a/building-financial-products-in-solana/figures/vault-deposit.svg +++ b/building-financial-products-in-solana/figures/fund-deposit.svg @@ -111,11 +111,11 @@ - vault-strategy program + managed-fund program initialize_registry() approve_asset() - initialize_strategy() + initialize_fund() add_asset() set_weight() deposit() @@ -126,10 +126,10 @@ - "STRATEGY" + INDEX + "FUND" + INDEX - strategy + fund manager: Maria registry: Victor's @@ -143,7 +143,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 0 @@ -156,7 +156,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 1 @@ -169,52 +169,52 @@ - "SHARE_MINT" + STRATEGY + "SHARE_MINT" + FUND share mint supply: 900 shares - mint authority: the strategy PDA + mint authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + USDC MINT USDC vault 0 USDC - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + TSLAX MINT TSLAx vault 1.44 TSLAx - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + NVDAX MINT NVDAx vault 3 NVDAx - authority: the strategy PDA + authority: the fund PDA diff --git a/building-financial-products-in-solana/figures/vault-fee-dilution.svg b/building-financial-products-in-solana/figures/fund-fee-dilution.svg similarity index 100% rename from building-financial-products-in-solana/figures/vault-fee-dilution.svg rename to building-financial-products-in-solana/figures/fund-fee-dilution.svg diff --git a/building-financial-products-in-solana/figures/vault-rebalance.svg b/building-financial-products-in-solana/figures/fund-rebalance.svg similarity index 97% rename from building-financial-products-in-solana/figures/vault-rebalance.svg rename to building-financial-products-in-solana/figures/fund-rebalance.svg index 2fe8f3c2..46bd8bd8 100644 --- a/building-financial-products-in-solana/figures/vault-rebalance.svg +++ b/building-financial-products-in-solana/figures/fund-rebalance.svg @@ -106,11 +106,11 @@ owner: Pyth Receiver - vault-strategy program + managed-fund program initialize_registry() approve_asset() - initialize_strategy() + initialize_fund() add_asset() set_weight() deposit() @@ -121,10 +121,10 @@ - "STRATEGY" + INDEX + "FUND" + INDEX - strategy + fund manager: Maria registry: Victor's @@ -138,7 +138,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 0 @@ -151,7 +151,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 1 @@ -164,7 +164,7 @@ - "SHARE_MINT" + STRATEGY + "SHARE_MINT" + FUND share mint @@ -173,40 +173,40 @@ - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + USDC MINT USDC vault 0 USDC - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + TSLAX MINT TSLAx vault ≈1.54 TSLAx - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + NVDAX MINT NVDAx vault 2.88 NVDAx - authority: the strategy PDA + authority: the fund PDA diff --git a/building-financial-products-in-solana/figures/vault-registry.svg b/building-financial-products-in-solana/figures/fund-registry.svg similarity index 99% rename from building-financial-products-in-solana/figures/vault-registry.svg rename to building-financial-products-in-solana/figures/fund-registry.svg index 44012f8b..c3190921 100644 --- a/building-financial-products-in-solana/figures/vault-registry.svg +++ b/building-financial-products-in-solana/figures/fund-registry.svg @@ -107,11 +107,11 @@ - vault-strategy program + managed-fund program initialize_registry() approve_asset() - initialize_strategy() + initialize_fund() add_asset() set_weight() deposit() diff --git a/building-financial-products-in-solana/figures/vault-withdraw-in-kind.svg b/building-financial-products-in-solana/figures/fund-withdraw-in-kind.svg similarity index 97% rename from building-financial-products-in-solana/figures/vault-withdraw-in-kind.svg rename to building-financial-products-in-solana/figures/fund-withdraw-in-kind.svg index 501bc1f2..f19aab08 100644 --- a/building-financial-products-in-solana/figures/vault-withdraw-in-kind.svg +++ b/building-financial-products-in-solana/figures/fund-withdraw-in-kind.svg @@ -135,11 +135,11 @@ - vault-strategy program + managed-fund program initialize_registry() approve_asset() - initialize_strategy() + initialize_fund() add_asset() set_weight() deposit() @@ -150,10 +150,10 @@ - "STRATEGY" + INDEX + "FUND" + INDEX - strategy + fund manager: Maria registry: Victor's @@ -167,7 +167,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 0 @@ -180,7 +180,7 @@ - "ASSET" + STRATEGY + INDEX + "ASSET" + FUND + INDEX asset config 1 @@ -193,52 +193,52 @@ - "SHARE_MINT" + STRATEGY + "SHARE_MINT" + FUND share mint supply: 463.5 shares - mint authority: the strategy PDA + mint authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + USDC MINT USDC vault 0 USDC - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + TSLAX MINT TSLAx vault ≈0.78 TSLAx - authority: the strategy PDA + authority: the fund PDA - STRATEGY'S ADDRESS + + FUND'S ADDRESS + TOKEN PROGRAM + NVDAX MINT NVDAx vault ≈1.47 NVDAx - authority: the strategy PDA + authority: the fund PDA diff --git a/building-financial-products-in-solana/lending.md b/building-financial-products-in-solana/lending.md index 9bdda46c..e2aa4e69 100644 --- a/building-financial-products-in-solana/lending.md +++ b/building-financial-products-in-solana/lending.md @@ -165,6 +165,6 @@ Six concepts came out of the lending market: - **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. -In the *Vault Strategy* 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 money 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/vault-strategy.md b/building-financial-products-in-solana/managed-fund.md similarity index 65% rename from building-financial-products-in-solana/vault-strategy.md rename to building-financial-products-in-solana/managed-fund.md index 3da26c98..2dcd01bf 100644 --- a/building-financial-products-in-solana/vault-strategy.md +++ b/building-financial-products-in-solana/managed-fund.md @@ -1,34 +1,34 @@ -# Chapter 18: The Vault Strategy +# Chapter 18: The Managed Fund -A **vault strategy** is the onchain equivalent of an index 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 vault-strategy platforms on Solana. The program lives at `finance/vault-strategy` in the companion repository, and its story trades stocks: TSLAx and NVDAx, quoted in USDC. +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. -One vocabulary note before the story, because the word is overloaded. On Solana, a **vault** is a token account owned by a program; that is how this book has used it in every chapter. Some platforms call the whole fund product "a vault." This book does not: here **the product is the strategy, and the token accounts are the vaults.** +> **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 strategies 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. 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. -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 strategy, the feed address is copied out of the `ApprovedAsset` account; the manager never supplies a price feed. +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. The approved set is stocks, bonds, whatever investors actually want. It cannot include the manager's own token: `add_asset()` checks approval by deriving the `ApprovedAsset` address for the mint it is handed, so adding an unapproved asset fails because the account does not exist (checked by `test_add_asset_rejects_unapproved`). -![](figures/vault-registry.svg) +![](figures/fund-registry.svg) -## The Manager Creates the Strategy and Sets the Weights +## The Manager Creates the Fund and Sets the Weights -Maria is the fund manager: she creates the strategy, sets the weights, rebalances, and earns the fee. She runs `initialize_strategy()` with index 0, her fee of 100 basis points (1%), a `max_slippage_bps` of 100, and the swap router the strategy 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. 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. -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%** (`StrategyNotFullyAllocated`). A deposit into this strategy 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`. +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`. -![](figures/vault-create-strategy.svg) +![](figures/fund-create.svg) Financial state on Solana is accounts, and Maria's three transactions created the seven the fund needs. -The **strategy** account (PDA seeds: `"strategy"`, an index) records the `manager`, the `registry` it is bound to, its `fee_bps` and `max_slippage_bps`, the `swap_router` it is allowed to trade through, its running `total_shares` and `total_weight_bps`, and `last_fee_accrual_timestamp` for the fee clock. It also records `usdc_holdings` and `asset_holdings`, the amounts the program has counted into its vaults. The **share mint** lives at a PDA (seeds: `"share_mint"`, the strategy) with the strategy PDA as mint authority: shares only come into existence when the program's own math says so. The strategy PDA is also the authority of a **USDC vault**, which every deposit passes through on its way into the basket. +The **fund** account (PDA seeds: `"fund"`, an index) records the `manager`, the `registry` it is bound to, its `fee_bps` and `max_slippage_bps`, the `swap_router` it is allowed to trade through, its running `total_shares` and `total_weight_bps`, and `last_fee_accrual_timestamp` for the fee clock. It also records `usdc_holdings` and `asset_holdings`, the amounts the program has counted into its vaults. The **share mint** lives at a PDA (seeds: `"share_mint"`, the fund) with the fund PDA as mint authority: shares only come into existence when the program's own math says so. The fund PDA is also the authority of a **USDC vault**, which every deposit passes through on its way into the basket. -Each basket asset gets an **asset config** account (PDA seeds: `"asset"`, the strategy, 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 strategy PDA. The strategy 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. +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. @@ -38,19 +38,19 @@ 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 strategy 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 strategy 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 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`). -**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 strategy'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.** +**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.** -![](figures/vault-deposit.svg) +![](figures/fund-deposit.svg) ## 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. -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 strategy-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 money moved between fund-owned vaults, through the approved router, at oracle-checked prices, and nowhere near Maria. -![](figures/vault-rebalance.svg) +![](figures/fund-rebalance.svg) ## A Later Investor Buys In at the New Price @@ -58,9 +58,9 @@ Bob, the second investor, deposits 480 USDC of his standard wallet. He is not ea ## 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 strategy'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 strategy 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 snapshotted a fee per event; the same principle, at fund scale. -![](figures/vault-fee-dilution.svg) +![](figures/fund-fee-dilution.svg) ## Withdrawing In Kind @@ -68,7 +68,7 @@ Alice takes profit with `withdraw()`, burning her 900 shares. The program does n She entered with $900, rode NVIDIA up, paid Maria 1% by dilution, and left with about $950 of assets. Bob still holds 450 shares, unrealized. Maria holds 13.5 shares, about $14, for managing the fund. Victor took no fee on any of it. What he earned is what his platform sells: a track record of funds whose managers cannot take the assets. -![No price account appears here: every leg is a proportion of a vault balance, not a quote.](figures/vault-withdraw-in-kind.svg) +![No price account appears here: every leg is a proportion of a vault balance, not a quote.](figures/fund-withdraw-in-kind.svg) ## The Math: One Valuation Loop, Floors Toward the Fund @@ -76,7 +76,7 @@ Every formula in the program is a proportion, and they all round the same way. S ## The Invariants: Custody Never Moves -- From first deposit to last withdrawal, assets live only in strategy-owned vaults. No instruction handler transfers vault tokens to the manager; the fee path mints shares and touches no vault. +- 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. @@ -86,20 +86,20 @@ Every formula in the program is a proportion, and they all round the same way. S ## 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. -- **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 strategy should, and the *Offchain Truth* chapter explains what confidence means and why "fresh but uncertain" is its own failure mode. +- **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. - **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 strategies 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` 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. -## What You Learnt from Building the Vault Strategy +## What You Learnt from Building the Managed Fund -An index fund needs a custodian, a compliance officer, a fee schedule in a prospectus, and an auditor checking that the manager's trades match the mandate. This chapter needed a registry account, a weight sum that must hit 10,000, a fee constant with no setter, and a slippage floor computed from a feed the manager cannot choose. +A mutual fund needs a custodian, a compliance officer, a fee schedule in a prospectus, and an auditor checking that the manager's trades match the mandate. This chapter needed a registry account, a weight sum that must hit 10,000, a fee constant with no setter, and a slippage floor computed from a feed the manager cannot choose. -The vault added five concepts: +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. - **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. diff --git a/building-financial-products-in-solana/perpetual-futures.md b/building-financial-products-in-solana/perpetual-futures.md index 157cfc7d..675a6478 100644 --- a/building-financial-products-in-solana/perpetual-futures.md +++ b/building-financial-products-in-solana/perpetual-futures.md @@ -33,7 +33,7 @@ The pool is the mint authority of the **LP share mint** (seeds `"lp_mint"`) and ## 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 *Vault Strategy* 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 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. 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. diff --git a/conclusion/where-to-go-from-here.md b/conclusion/where-to-go-from-here.md index 2581a4cb..98d1450f 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 vault strategy, 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 machinery most financial products are made of. 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. @@ -43,7 +43,7 @@ So aim at consumers and businesses rather than at a crypto audience. In practice 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. -**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 *Vault Strategy* 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. +**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. **Savings that pay what borrowers pay.** The deposit side of the *Lending and Borrowing* chapter, with a balance, a rate, and a withdraw button in front of it and nothing else on the screen. The versions that hold up are the ones that say the awkward part up front. Deposits earn interest because they are lent out, so when most of the pool is on loan, the cash left in the reserve may not cover a withdrawal until borrowers repay. A depositor should read that on the screen before they deposit, not learn it from a refused withdrawal. @@ -63,7 +63,7 @@ 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 vault instead, redeemable at a share price at any hour. The *Vault Strategy* 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 is the machinery, 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. @@ -71,7 +71,7 @@ In each of these cases there is a specific situation, an individual or organizat The better ideas are in the places where the machinery exists 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 *Vault Strategy* 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 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. **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. diff --git a/financial-programming-concepts-in-solana/offchain-truth.md b/financial-programming-concepts-in-solana/offchain-truth.md index a40d7c8f..fb61ed7f 100644 --- a/financial-programming-concepts-in-solana/offchain-truth.md +++ b/financial-programming-concepts-in-solana/offchain-truth.md @@ -1,6 +1,6 @@ # 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 vault strategy. 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: 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. ## An Oracle's Publication Is an Input to Check @@ -12,8 +12,8 @@ The second is **published to an account**: the key signs its statement offchain, 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 vault strategy 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 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 vault strategy 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 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 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] diff --git a/financial-programming-concepts-in-solana/operating-financial-software.md b/financial-programming-concepts-in-solana/operating-financial-software.md index 4923aa5c..7b7bc61e 100644 --- a/financial-programming-concepts-in-solana/operating-financial-software.md +++ b/financial-programming-concepts-in-solana/operating-financial-software.md @@ -51,8 +51,8 @@ The rule underneath is to match the level to how reversible your reaction is, be 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. -- **Bound what configuration can express.** Validate every parameter at write time against a limit that makes abuse impossible by configuration. The vault strategy 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 vault strategy'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. +- **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. diff --git a/financial-programming-concepts-in-solana/the-adversary.md b/financial-programming-concepts-in-solana/the-adversary.md index 603cd578..a14b59e2 100644 --- a/financial-programming-concepts-in-solana/the-adversary.md +++ b/financial-programming-concepts-in-solana/the-adversary.md @@ -35,7 +35,7 @@ A missing check lets the attacker **substitute** an account: pass a different ac 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. -Take a pool that prices its shares from the token balance in its vault, as the book's vault strategy 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: +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: shares minted = floor(deposit × shares outstanding ÷ pool value) @@ -49,14 +49,14 @@ The pool's value here is the USDC in its vault. USDC has six decimals, so a mino The attack pays exactly what Bob deposited. A larger deposit loses less, but still loses. Had Bob deposited 1,500 USDC, he would have received floor(1,500 × 1 ÷ 1,000.000001) = floor(1.4999) = 1 share. The two shares would then split 2,500.000001 USDC, and Bob's 1,500 would have become 1,250. The fraction of a share that the floor drops stays in the vault, so it adds to the value of the shares that already exist. The attacker can leave the pool in this state and collect that fraction from every later depositor. Taking a whole deposit needs a donation at least as large as the deposit, so the usual form of the attack is front-running: the attacker sees a deposit pending, sizes the donation to match it, and gets both of their transactions ordered ahead of it. -Bob can protect his own deposit with a minimum-output bound. The vault strategy's deposit handler takes a `minimum_shares` argument and rejects with `SlippageTooHigh` when it would mint fewer shares. That protects only the depositors whose wallet or app sets the argument. The vault strategy keeps it, for a share price that moves between signing and landing, but does not rely on it. The defenses below are in the program, so they protect every depositor, and each one can be checked against the same numbers: +Bob can protect his own deposit with a minimum-output bound. The managed fund's deposit handler takes a `minimum_shares` argument and rejects with `SlippageTooHigh` when it would mint fewer shares. That protects only the depositors whose wallet or app sets the argument. The managed fund keeps it, for a share price that moves between signing and landing, but does not rely on it. The defenses below are in the program, so they protect every depositor, and each one can be checked against the same numbers: - **Lock some shares that no one owns.** On the first deposit, the program adds a fixed number of shares to the supply without minting them to anyone, so the supply never starts at dust. Say 100 shares are locked, the size of the AMM's `MINIMUM_LIQUIDITY` floor, and every formula counts them in the supply it divides by. The attacker's smallest position is then one share out of 101. Pricing each share at 1,000 USDC takes a donation of about 101,000 USDC, and 100 of every 101 units of it belong to shares nobody can withdraw. The attacker spends 101,000 USDC to zero out Bob's 1,000, then can withdraw only about 1,010 USDC. - **Add virtual shares and virtual value to the formula.** The program adds a fixed number of shares to the supply and a fixed amount to the pool's value, but only inside the division, so it works normally even when no real shares exist. Say a million virtual shares and one virtual minor unit. The attacker's one-minor-unit deposit now buys a million shares, and the donation is split between their million and the virtual million. Bob's 1,000 USDC buys 1,999,999 shares, which redeem for 999.99975 USDC. The attacker's shares redeem for about 500 USDC, and the other half of the donation belongs to the virtual shares, which nobody can withdraw. -- **Ignore donations.** The program records the pool's size in its own state, as the lending market's `available_liquidity` and the vault strategy's recorded holdings do, rather than reading the vault's token balance. Step 2 then changes the vault's balance and nothing the handler reads. The pool is still one minor unit against one share, and Bob's deposit mints a share per minor unit as it should. The donated 1,000 USDC sits in the vault outside the program's accounting, lost to the attacker, and the lending example tests this with `raw_token_donation_does_not_inflate_exchange_rate`. The vault strategy's `test_donation_does_not_inflate_share_price` runs these exact numbers. A recorded size stops donations only while the attacker has no other way to raise it. The *Perpetual Futures* chapter shows one: a provider who is also the pool's only trader can pay funding into it. The *Lending and Borrowing* chapter shows another: a lending reserve's size counts the interest its borrowers owe, and a supplier may borrow from their own reserve. One minor unit of interest is enough to start. After that, the fraction each of the attacker's own deposits loses to the floor stays in the pool and raises their share further. +- **Ignore donations.** The program records the pool's size in its own state, as the lending market's `available_liquidity` and the managed fund's recorded holdings do, rather than reading the vault's token balance. Step 2 then changes the vault's balance and nothing the handler reads. The pool is still one minor unit against one share, and Bob's deposit mints a share per minor unit as it should. The donated 1,000 USDC sits in the vault outside the program's accounting, lost to the attacker, and the lending example tests this with `raw_token_donation_does_not_inflate_exchange_rate`. The managed fund's `test_donation_does_not_inflate_share_price` runs these exact numbers. A recorded size stops donations only while the attacker has no other way to raise it. The *Perpetual Futures* chapter shows one: a provider who is also the pool's only trader can pay funding into it. The *Lending and Borrowing* chapter shows another: a lending reserve's size counts the interest its borrowers owe, and a supplier may borrow from their own reserve. One minor unit of interest is enough to start. After that, the fraction each of the attacker's own deposits loses to the floor stays in the pool and raises their share further. - **Require a large first deposit.** If the first deposit must be at least 100 USDC, the attacker's smallest position is 100,000,000 shares. Rounding Bob's 1,000 USDC down to nothing would then take a share price above 1,000 USDC, a vault holding a hundred billion dollars. -A program needs at least one of these four, chosen on purpose, and has to check that nothing else it allows raises the pool's value the way a donation does. The book's programs each name theirs. The AMM locks shares, and the vault strategy ignores donations. The lending market and the perpetual futures pool do both. Each ignores donations by pricing shares from figures it records rather than its vault's balance. Each also locks shares, because its own users can raise those figures: borrowers with the interest they owe, traders with the funding they pay. +A program needs at least one of these four, chosen on purpose, and has to check that nothing else it allows raises the pool's value the way a donation does. The book's programs each name theirs. The AMM locks shares, and the managed fund ignores donations. The lending market and the perpetual futures pool do both. Each ignores donations by pricing shares from figures it records rather than its vault's balance. Each also locks shares, because its own users can raise those figures: borrowers with the interest they owe, traders with the funding they pay. ## A True Price Can Still Be a Manipulated Price @@ -87,7 +87,7 @@ The rule is **checks-effects-interactions**: validate everything, then update yo Two more rules for these boundaries: -- **Validate what a CPI returns and what it was given.** Calling a program does not make the accounts it touched trustworthy. The vault strategy computes each swap's minimum output from the oracle, because the router it calls is outside its control. +- **Validate what a CPI returns and what it was given.** Calling a program does not make the accounts it touched trustworthy. The managed fund computes each swap's minimum output from the oracle, because the router it calls is outside its control. - **Assume the instructions after yours are hostile.** Any invariant your program needs must hold at the end of *your* instruction, not at the end of the transaction, because the instructions after yours belong to someone else. The *Arithmetic of Money* chapter's postcondition checks, such as re-checking K after the transfers land, follow the same rule: check your own state before anyone else's code runs. 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 82517557..b88b4e11 100644 --- a/financial-programming-concepts-in-solana/the-arithmetic-of-money.md +++ b/financial-programming-concepts-in-solana/the-arithmetic-of-money.md @@ -57,7 +57,7 @@ The table covers money changing hands. The same direction applies to the numbers ### A Ceiled Fee Is a Capped Fee -Rounding up must never make a fee larger than the amount it is charged on. The order book backs its ceiling division with a check that the fee never exceeds the gross. Fee *parameters* are bounded when they are written, too: the order book refuses a fee rate above 100%, and the vault strategy caps its manager's fee at 10%. No configuration can express a fee its program does not allow, so no manager can abuse one to drain the fund. +Rounding up must never make a fee larger than the amount it is charged on. The order book backs its ceiling division with a check that the fee never exceeds the gross. Fee *parameters* are bounded when they are written, too: the order book refuses a fee rate above 100%, and the managed fund caps its manager's fee at 10%. No configuration can express a fee its program does not allow, so no manager can abuse one to drain the fund. ### Ceiling Debt Is What "Paid in Full" Means Onchain diff --git a/scripts/check-figure-bands.mjs b/scripts/check-figure-bands.mjs index 20959c58..05410a54 100644 --- a/scripts/check-figure-bands.mjs +++ b/scripts/check-figure-bands.mjs @@ -20,7 +20,7 @@ // It over-reports on figures whose content is a curve rather than boxes: a // steep chart line covers few columns and scores as DEAD. Six such bands are // known and deliberate as of writing — amm-impermanent-loss (2), -// lending-rate-curve, vault-fee-dilution, rent-lifecycle, and the caption gap +// lending-rate-curve, fund-fee-dilution, rent-lifecycle, and the caption gap // in escrow-swap-intent. So --strict is for checking a figure you just edited // (`--strict path/to/one.svg`), not for gating the whole book. // diff --git a/scripts/check-figure-geometry.mjs b/scripts/check-figure-geometry.mjs index 466ddf86..e311c04d 100644 --- a/scripts/check-figure-geometry.mjs +++ b/scripts/check-figure-geometry.mjs @@ -504,7 +504,7 @@ for (const file of selected) { "building-financial-products-in-solana/figures/lending-borrow-ltv.svg 186,346 over obligation", "building-financial-products-in-solana/figures/lending-borrow-ltv.svg 186,346 over collateral vault", "building-financial-products-in-solana/figures/order-book-settle.svg 229,148 over Bob's USDC", - "building-financial-products-in-solana/figures/vault-withdraw-in-kind.svg 240,318 over seed label \"TOKEN PROGRAM + TSLAX MI\"", + "building-financial-products-in-solana/figures/fund-withdraw-in-kind.svg 240,318 over seed label \"TOKEN PROGRAM + TSLAX MI\"", ]); // 9. An arrow meets an account box on the flat of an edge, aimed into the // box. A head that lands on the rounded corner covers the corner, and a diff --git a/scripts/lint-manuscript.mjs b/scripts/lint-manuscript.mjs index 36e79431..9a1d6381 100644 --- a/scripts/lint-manuscript.mjs +++ b/scripts/lint-manuscript.mjs @@ -273,7 +273,7 @@ const chapterTitles = [ "Constant-Product AMM", "Prop AMM", "Lending and Borrowing", - "Vault Strategy", + "Managed Fund", "Perpetual Futures", "Where to Go From Here", ]; diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index 3792055a..ec98d439 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -83,7 +83,7 @@ A program that needs a long span in wall-clock time should store a timestamp bes ## The Wall Clock Measures Human Time -The vault strategy uses the wall clock twice: +The managed fund uses the wall clock twice: - **Its annual management fee accrues by timestamp**, because a few seconds either way, measured against a year, are nothing. - **It checks its price feed's age in seconds rather than slots.** A **price feed** is an account that some publisher keeps updated with a price and the time they published it. This one publishes a wall-clock time, so the comparison has to be made in the unit the number arrived in. @@ -108,7 +108,7 @@ The *Offchain Truth* chapter answers the first, along with everything else a pro | the lending market | 25 slots | slots | | the prop AMM | 150 slots | slots | | the perpetual futures venue | 150 slots | slots | -| the vault strategy | 60 seconds | seconds | +| the managed fund | 60 seconds | seconds | For a price fresh to within two seconds, turn that into a slot count once, and check every price against that number of slots. Nobody can move that boundary, and the check needs no conversion. Three of the four count slots, because in a window this short a few seconds of clock would be most of the margin. The vault counts seconds because that is the unit its feed publishes, and a program cannot compare someone else's seconds against its own slots without assuming a slot length. What the three slot counts come to in seconds depends on that length, so a window sized once in slots covers less wall-clock time if the slot is ever shortened. @@ -147,7 +147,7 @@ No block is produced during a halt, so the slot count does not move. Wall-clock - **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. -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 vault strategy 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. +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. The order book comes through a halt untouched, because it reads neither clock: queue positions resume exactly as they were. @@ -171,7 +171,7 @@ The table lists every program in Part II that touches time, and the clock each o | 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 | | 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 | -| vault strategy | an annual fee, and price freshness | timestamps for both | seconds against a year are nothing, and the feed stamps seconds | +| 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 | ## What You Learnt diff --git a/solana-building-blocks/tokens.md b/solana-building-blocks/tokens.md index bf5d98be..31ef4fe6 100644 --- a/solana-building-blocks/tokens.md +++ b/solana-building-blocks/tokens.md @@ -14,7 +14,7 @@ Each kind of token is defined by its own **mint account**. - `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` defines USDC on mainnet [^usdc-address] - `Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh` defines NVDAx on mainnet [^nvdax-address] -- An address you will create in the *Vault Strategy* chapter defines the shares that fund issues (on devnet, and on mainnet too if you choose to deploy it for real). +- An address you will create in the *Managed Fund* chapter defines the shares that fund issues (on devnet, and on mainnet too if you choose to deploy it for real). 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. From 74242a9bbd2fe406d792ca01f13b5fb6e92695e9 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Mon, 28 Sep 2026 21:40:24 +0000 Subject: [PATCH 2/2] Rename the Token Fundraiser chapter to The Fundraiser Contributors receive no token, only a refund if the target is missed, so "Token" described something the program does not do. The companion repository renames finance/token-fundraiser to finance/fundraiser to match. - The chapter becomes fundraiser.md, titled The Fundraiser, and names finance/fundraiser. - Cross-references, the table of contents, the Amazon description, the glossary, the writing plan and the Time chapter's table follow. - The linter's chapter-title list drops the title: "Fundraiser" alone is an ordinary word, like Escrow, so the convention covers it instead. Claude-Session: https://claude.ai/code/session_01JTaqedahgCHQbzXPcHDPok --- AMAZON-DESCRIPTION.md | 2 +- TABLE-OF-CONTENTS.md | 2 +- WRITING-PLAN.md | 2 +- appendices/glossary.md | 2 +- building-financial-products-in-solana/escrow.md | 2 +- .../{token-fundraiser.md => fundraiser.md} | 6 +++--- building-financial-products-in-solana/perpetual-futures.md | 2 +- building-financial-products-in-solana/prop-amm.md | 2 +- conclusion/where-to-go-from-here.md | 2 +- scripts/lint-manuscript.mjs | 5 ++--- solana-building-blocks/time.md | 4 ++-- 11 files changed, 15 insertions(+), 16 deletions(-) rename building-financial-products-in-solana/{token-fundraiser.md => fundraiser.md} (96%) diff --git a/AMAZON-DESCRIPTION.md b/AMAZON-DESCRIPTION.md index 946c710e..ded9e07d 100644 --- a/AMAZON-DESCRIPTION.md +++ b/AMAZON-DESCRIPTION.md @@ -10,7 +10,7 @@ Written for engineers shipping software that holds other people's money, and for **Part I** builds the fundamentals: Solana's accounts, tokens, PDAs, and time, then financial software as a state machine, the arithmetic of money, offchain truth, the adversary, and operating a program in production. Exploit classes are taught through the incidents that made them famous, among them the $2.6B SPL token-lending rounding bug, Cashio ($52M), Wormhole ($325M), and Mango Markets ($116M). -**Part II** walks ten complete, tested programs end to end: an escrow, an all-or-nothing token fundraiser, a parimutuel betting market, an order book exchange with a critbit matching engine, a constant-product AMM, an oracle-priced proprietary AMM, a lending and borrowing market with per-second interest and a liquidation engine, a managed fund (what Solana platforms often call a vault strategy), a fully collateralized options venue, and a perpetual futures venue with funding, margin, and pre-funded solvency. Every chapter gives the account model, the instruction handlers walked as a story, the math in minor units with its rounding directions, the invariants, the failure modes, the tests that catch each one, and the incentives of everyone involved, operator and user alike. Account maps, state machines, and value flows are drawn in one visual language across all ten, so the programs can be read against each other. They have not been explained side by side in one place before. +**Part II** walks ten complete, tested programs end to end: an escrow, an all-or-nothing fundraiser, a parimutuel betting market, an order book exchange with a critbit matching engine, a constant-product AMM, an oracle-priced proprietary AMM, a lending and borrowing market with per-second interest and a liquidation engine, a managed fund (what Solana platforms often call a vault strategy), a fully collateralized options venue, and a perpetual futures venue with funding, margin, and pre-funded solvency. Every chapter gives the account model, the instruction handlers walked as a story, the math in minor units with its rounding directions, the invariants, the failure modes, the tests that catch each one, and the incentives of everyone involved, operator and user alike. Account maps, state machines, and value flows are drawn in one visual language across all ten, so the programs can be read against each other. They have not been explained side by side in one place before. In the tradition of *TCP/IP Illustrated*, the book teaches the layer that does not change. Frameworks churn. The account model has been stable since Solana launched, the arithmetic of money will not change, and neither will the desire of adversaries to take your assets. diff --git a/TABLE-OF-CONTENTS.md b/TABLE-OF-CONTENTS.md index 9cff0410..08fe99f7 100644 --- a/TABLE-OF-CONTENTS.md +++ b/TABLE-OF-CONTENTS.md @@ -28,7 +28,7 @@ Each chapter explains one complete, tested program from the [companion repository](https://github.com/quicknode/solana-program-examples). - [Chapter 11: Escrow](building-financial-products-in-solana/escrow.md) - two strangers swap without trusting each other -- [Chapter 12: Token Fundraiser](building-financial-products-in-solana/token-fundraiser.md) - onchain crowdfunding +- [Chapter 12: The Fundraiser](building-financial-products-in-solana/fundraiser.md) - onchain crowdfunding - [Chapter 13: The Betting Market](building-financial-products-in-solana/betting-market.md) - a parimutuel pool with no bookmaker - [Chapter 14: The Order Book Exchange](building-financial-products-in-solana/order-book.md) - a traditional NYSE/NASDAQ/CME style exchange - [Chapter 15: The Constant-Product AMM](building-financial-products-in-solana/constant-product-amm.md) - instant swaps priced by a formula diff --git a/WRITING-PLAN.md b/WRITING-PLAN.md index 79eccd55..6646fec1 100644 --- a/WRITING-PLAN.md +++ b/WRITING-PLAN.md @@ -85,7 +85,7 @@ Steps currently missing their per-step diagram, to be drawn in the visual pass: ## Milestones - **Voice samples first** (drafted): the Introduction, Escrow, and The Order Book Exchange lock voice, depth, and template. -- The remaining program chapters, one per example, in table-of-contents order (all ten drafted: Escrow, Token Fundraiser, The Betting Market, The Order Book Exchange, The Constant-Product AMM, The Prop AMM, Lending and Borrowing, The Managed Fund, Options, Perpetual Futures). They run as three groups: custody and pooling, then market making, then credit and derivatives. A chapter's place is set by what it assumes the reader already has, so The Betting Market sits third, where its only prerequisite is Escrow, and Options sits just before Perpetual Futures, because a fully collateralized derivative is the case the leveraged one relaxes. +- The remaining program chapters, one per example, in table-of-contents order (all ten drafted: Escrow, The Fundraiser, The Betting Market, The Order Book Exchange, The Constant-Product AMM, The Prop AMM, Lending and Borrowing, The Managed Fund, Options, Perpetual Futures). They run as three groups: custody and pooling, then market making, then credit and derivatives. A chapter's place is set by what it assumes the reader already has, so The Betting Market sits third, where its only prerequisite is Escrow, and Options sits just before Perpetual Futures, because a fully collateralized derivative is the case the leveraged one relaxes. - The two Fundamentals sections (Solana Building Blocks; Financial Programming Concepts in Solana), with technical reviewers engaged per chapter as each completes. - The remaining appendices. The Glossary is drafted. - The placeholder front-matter pages last. diff --git a/appendices/glossary.md b/appendices/glossary.md index 712a47f0..d584504e 100644 --- a/appendices/glossary.md +++ b/appendices/glossary.md @@ -18,7 +18,7 @@ A market maker's structural loss: the trades that find you when your quote is wr ### all-or-nothing -A fundraising rule under which contributions bind only if the full target is reached by the deadline; otherwise every contributor is refunded in full. The *Token Fundraiser* chapter builds it onchain. +A fundraising rule under which contributions bind only if the full target is reached by the deadline; otherwise every contributor is refunded in full. The *Fundraiser* chapter builds it onchain. ### American option / European option diff --git a/building-financial-products-in-solana/escrow.md b/building-financial-products-in-solana/escrow.md index 58dc7ab6..a4255a66 100644 --- a/building-financial-products-in-solana/escrow.md +++ b/building-financial-products-in-solana/escrow.md @@ -151,6 +151,6 @@ The escrow gave you five concepts: - **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. - **Minor units.** Every amount is an integer at the mint's declared scale, and identical integers can mean entirely different things. -In the *Token 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 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. [^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/token-fundraiser.md b/building-financial-products-in-solana/fundraiser.md similarity index 96% rename from building-financial-products-in-solana/token-fundraiser.md rename to building-financial-products-in-solana/fundraiser.md index 7f3a9c96..94273a0f 100644 --- a/building-financial-products-in-solana/token-fundraiser.md +++ b/building-financial-products-in-solana/fundraiser.md @@ -1,8 +1,8 @@ -# Chapter 12: Token Fundraiser +# 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**. That rule is this chapter's program: an all-or-nothing fundraiser, the same promise Kickstarter makes, enforced by code instead of by a company. -The program lives at `finance/token-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 custody machinery is the same, with one addition: a **deadline**. +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 custody machinery is the same, with one addition: a **deadline**. The program calls the fundraiser's creator the **maker**: Maria here, and her incentive is straightforward: she gets her production run funded all-or-nothing, because 3,600 USDC of a 9,000 USDC mold is scrap metal. There is no platform operator and no fee in this program; the maker runs their own raise, and the only costs anyone pays are rent deposits on the accounts. @@ -112,7 +112,7 @@ Every addition and subtraction on `current_amount` and the receipts is checked a The suite warps the clock with a helper that advances whole days, and asserts balances after every move. Its anchors are the paired lifecycle tests: `test_check_contributions_success_pays_maker_and_closes_vault` funds the raise with ten contributors at the cap and watches the claim, `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 proves the same wallet can raise again. Around them, one test per gate. 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` proves close is where strays are finally swept to the maker). -## What You Learnt from Building the Token Fundraiser +## 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 made the same promise a property of code: no platform, no fee, and terms that cannot change because no handler exists to change them. diff --git a/building-financial-products-in-solana/perpetual-futures.md b/building-financial-products-in-solana/perpetual-futures.md index 675a6478..e378566f 100644 --- a/building-financial-products-in-solana/perpetual-futures.md +++ b/building-financial-products-in-solana/perpetual-futures.md @@ -29,7 +29,7 @@ The **pool** account lives at a PDA with seeds `"pool"`, the collateral mint, an - `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 *Token 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 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. ## Liquidity Providers Fund the Pool and Get Shares Priced at Mark diff --git a/building-financial-products-in-solana/prop-amm.md b/building-financial-products-in-solana/prop-amm.md index 3bc52a5f..e76a7eec 100644 --- a/building-financial-products-in-solana/prop-amm.md +++ b/building-financial-products-in-solana/prop-amm.md @@ -25,7 +25,7 @@ Financial state on Solana is accounts, and this one transaction created the thre 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 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 *Token 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 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. 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. diff --git a/conclusion/where-to-go-from-here.md b/conclusion/where-to-go-from-here.md index 98d1450f..9ed14435 100644 --- a/conclusion/where-to-go-from-here.md +++ b/conclusion/where-to-go-from-here.md @@ -55,7 +55,7 @@ Products of this kind have been shipped already, several of them with real users **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. -**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 *Token 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. +**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. **Cash today for an invoice due in sixty days.** A supplier ships forty thousand dollars of goods on sixty-day terms and needs the money this week, so funders advance most of the face value and are repaid when the buyer pays. Pooling and interest come from the *Lending and Borrowing* chapter, repayment runs through escrow, and whether the invoice was really paid is an offchain fact, which makes the *Offchain Truth* chapter the one this product depends on most. The credit risk stays where it always was, on the buyer. What goes away is the underwriting time and the size floor. diff --git a/scripts/lint-manuscript.mjs b/scripts/lint-manuscript.mjs index 9a1d6381..2f9a47b8 100644 --- a/scripts/lint-manuscript.mjs +++ b/scripts/lint-manuscript.mjs @@ -257,8 +257,8 @@ for (const file of [...new Set(files)]) { // "The" is dropped inside the italics, because "the *The Adversary* chapter" // reads badly: write "the *Adversary* chapter". // -// Only the distinctive multi-word titles are checked. Time, Tokens, PDAs and -// Escrow are ordinary words far too often to match mechanically, so the +// Only the distinctive multi-word titles are checked. Time, Tokens, PDAs, +// Escrow and Fundraiser are ordinary words far too often to match mechanically, so the // convention covers them and this check does not. const chapterTitles = [ @@ -267,7 +267,6 @@ const chapterTitles = [ "Offchain Truth", "Adversary", "Operating Financial Software", - "Token Fundraiser", "Betting Market", "Order Book Exchange", "Constant-Product AMM", diff --git a/solana-building-blocks/time.md b/solana-building-blocks/time.md index ec98d439..ed03f47b 100644 --- a/solana-building-blocks/time.md +++ b/solana-building-blocks/time.md @@ -114,7 +114,7 @@ For a price fresh to within two seconds, turn that into a slot count once, and c ## Integer Day Counts Have a Boundary, and Both Sides Must Use It -A clock gives a program seconds, so counting days means dividing by 86,400. That division is integer division, which throws the remainder away: six and a half days counts as six, and anything short of a full day counts as none of it. The token fundraiser stores `time_started` and a duration in days, and both of its later handlers reconstruct elapsed days the same way: +A clock gives a program seconds, so counting days means dividing by 86,400. That division is integer division, which throws the remainder away: six and a half days counts as six, and anything short of a full day counts as none of it. The fundraiser stores `time_started` and a duration in days, and both of its later handlers reconstruct elapsed days the same way: ``` elapsed days = (now − time_started) / 86,400 @@ -167,7 +167,7 @@ The table lists every program in Part II that touches time, and the clock each o | Program | What it times | Its answer | Why | | --- | --- | --- | --- | -| token 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 | +| 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 | | 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 |