From 1af56b11983e44206c0da3bc3f6e742df36c0cfa Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Thu, 10 Sep 2026 01:13:32 +0000 Subject: [PATCH] Let each market's state account own its vaults The token swap, prop AMM, perpetual futures, and options venues each had a dataless "authority" PDA beside their state account, existing only to own the vaults and sign for them. A program-owned data account signs with its own seeds just as well, as the escrow's offer account already did, and the seeds never change however the account's data does, so the extra account bought nothing but one more account per instruction. The companion repository now has the pool config, market, or pool own its vaults and mints directly, and the four chapters and their sixteen figures follow: the authority box is gone from every account map and lifecycle figure, each vault's owner label names the state account, and the prose says the state account signs. Claude-Session: https://claude.ai/code/session_014RdEWCoCChaoKPhvcftHVG --- .../constant-product-amm.md | 8 +-- .../figures/amm-account-map.svg | 71 +++++++++---------- .../figures/amm-bob-swap.svg | 63 ++++++++-------- .../figures/amm-first-deposit.svg | 69 ++++++++---------- .../figures/options-account-map.svg | 48 ++++++------- .../figures/options-buy-call.svg | 44 +++++------- .../figures/options-buy-put.svg | 70 ++++++++---------- .../figures/options-collect-fees.svg | 46 +++++------- .../figures/options-collect-proceeds.svg | 48 ++++++------- .../figures/options-exercise.svg | 54 ++++++-------- .../figures/options-reclaim.svg | 46 +++++------- .../figures/options-write-call.svg | 46 +++++------- .../figures/options-write-put.svg | 44 +++++------- .../figures/perps-account-map.svg | 38 ++++------ .../figures/perps-liquidation.svg | 38 ++++------ .../figures/perps-open-position.svg | 53 ++++---------- .../figures/prop-amm-account-map.svg | 45 +++++------- .../options.md | 2 +- .../perpetual-futures.md | 8 +-- .../prop-amm.md | 4 +- 20 files changed, 352 insertions(+), 493 deletions(-) diff --git a/building-financial-products-in-solana/constant-product-amm.md b/building-financial-products-in-solana/constant-product-amm.md index 6e13f60a..a0918717 100644 --- a/building-financial-products-in-solana/constant-product-amm.md +++ b/building-financial-products-in-solana/constant-product-amm.md @@ -25,13 +25,13 @@ The deeper the pool, the less any one trade moves the price. ## The Account Model: One Config, One Pool per Pair, and a Virtual Fee Ledger -![The pool authority PDA owns both vaults and the LP mint, so no person can move pool tokens.](figures/amm-account-map.svg) +![The pool config account owns both vaults and the LP mint and signs for them, so no person can move pool tokens.](figures/amm-account-map.svg) The **config** is a singleton PDA (seed `"config"`) holding the `admin`, the trading `fee` in basis points, and the `admin_share_bps`, the slice of each fee that belongs to the admin rather than the LPs. One config per deployed program mirrors how real venues ship. Each trading pair gets a **pool config** PDA, seeded by the config and the two mints, with one rule enforced at creation: `mint_a` must sort below `mint_b`, or creation fails with `InvalidMintOrder`. Without canonical ordering, an (ACME, USDC) pool and a (USDC, ACME) pool would both exist and split the liquidity between them. -The tokens sit in two vaults, `pool_a` and `pool_b`, owned by a **pool authority** PDA, alongside the **LP mint** (`liquidity_provider_mint`), whose tokens are the receipts liquidity providers hold. +The tokens sit in two vaults, `pool_a` and `pool_b`, owned by the pool config account, alongside the **LP mint** (`liquidity_provider_mint`), whose tokens are the receipts liquidity providers hold. The pool config is a PDA, so the program can sign for the vaults and the mint with its seeds, and nobody else can. Two more fields are `admin_fees_owed_a` and `admin_fees_owed_b` on the pool config. The admin's share of fees is never transferred during a swap; it accrues in these counters as a *virtual claim* while the tokens physically sit in the vaults. So every piece of LP-facing math runs on **effective reserves**: the raw vault balance minus the admin's owed slice. The admin's fees do not boost LP yield, do not distort the price, and get swept out later in one transfer. Keeping who is owed what separate from where it sits is the order book's fee vault, done with accounting instead of an account. @@ -39,7 +39,7 @@ Two more fields are `admin_fees_owed_a` and `admin_fees_owed_b` on the pool conf Maria operates the AMM and is launching ACME today. She earns twice: an admin share of every trading fee, and LP fees on the liquidity she provides herself. Like every character in this book she starts with one SOL and 1,000 USDC; she also holds 400 ACME of founder stock. -Maria runs `initialize_config()` with a fee of 30 basis points and an admin share of 1,667 basis points, about one sixth of each fee, the classic Uniswap-style split. Then `initialize_pool()` for ACME/USDC, which creates the pool config, the authority, the LP mint, and the two empty vaults. +Maria runs `initialize_config()` with a fee of 30 basis points and an admin share of 1,667 basis points, about one sixth of each fee, the classic Uniswap-style split. Then `initialize_pool()` for ACME/USDC, which creates the pool config, the LP mint, and the two empty vaults. Empty pools cannot price anything, so Maria seeds it. She believes a fair launch price is $2.25, so she runs `deposit_liquidity()` with her 400 ACME and 900 USDC, the founder stock and nine-tenths of her cash. **The first deposit sets the price**, because the ratio she deposits *is* the ratio. X = 400, Y = 900, K = 360,000. @@ -79,7 +79,7 @@ Bob sells 20 of his 40.7 ACME. A sell is the same instruction with `input_is_tok ## The Admin Sweeps Its Share of the Fees -Whenever she likes, Maria runs `claim_admin_fees()`. It zeroes both owed counters and transfers exactly those amounts from the vaults to her token accounts, signed by the pool authority; anyone else is rejected (`test_claim_admin_fees_rejects_non_admin`), and claiming when nothing is owed fails with `NothingToClaim`. One transfer per token, however many swaps accrued it: the same consolidation logic as the order book's unsettled balances. +Whenever she likes, Maria runs `claim_admin_fees()`. It zeroes both owed counters and transfers exactly those amounts from the vaults to her token accounts, signed by the pool config; anyone else is rejected (`test_claim_admin_fees_rejects_non_admin`), and claiming when nothing is owed fails with `NothingToClaim`. One transfer per token, however many swaps accrued it: the same consolidation logic as the order book's unsettled balances. ## Withdrawing Realizes Impermanent Loss diff --git a/building-financial-products-in-solana/figures/amm-account-map.svg b/building-financial-products-in-solana/figures/amm-account-map.svg index 8bf01c4f..2a386d01 100644 --- a/building-financial-products-in-solana/figures/amm-account-map.svg +++ b/building-financial-products-in-solana/figures/amm-account-map.svg @@ -1,4 +1,4 @@ - + Every account a swap touches @@ -178,45 +178,38 @@ admin_fees_owed_a: 0 admin_fees_owed_b: 0 - + - CONFIG'S ADDRESS + ACME & - USDC MINTS + "AUTHORITY" - pool authority - an address with no account - - - - POOL AUTHORITY + - TOKEN PROGRAM + ACME MINT - - - pool_a: the pool's ACME - - 500 ACME - authority: the pool authority - - - - POOL AUTHORITY + - TOKEN PROGRAM + USDC MINT - - - pool_b: the pool's USDC - - 1,125 USDC - authority: the pool authority - - - - CONFIG'S ADDRESS + ACME & - USDC MINTS + "LIQUIDITY" - - - LP mint - - supply: ≈ 750 LP - authority: the pool authority + POOL CONFIG + + TOKEN PROGRAM + ACME MINT + + + pool_a: the pool's ACME + + 500 ACME + authority: the pool config + + + + POOL CONFIG + + TOKEN PROGRAM + USDC MINT + + + pool_b: the pool's USDC + + 1,125 USDC + authority: the pool config + + + + CONFIG'S ADDRESS + ACME & + USDC MINTS + "LIQUIDITY" + + + LP mint + + supply: ≈ 750 LP + authority: the pool config X = 500, Y = 1,125, K = 562,500 after both deposits; the price is Y ÷ X = $2.25 per ACME diff --git a/building-financial-products-in-solana/figures/amm-bob-swap.svg b/building-financial-products-in-solana/figures/amm-bob-swap.svg index 4fca773f..82ca3761 100644 --- a/building-financial-products-in-solana/figures/amm-bob-swap.svg +++ b/building-financial-products-in-solana/figures/amm-bob-swap.svg @@ -1,4 +1,4 @@ - + Bob's swap moves the price @@ -173,53 +173,46 @@ admin_fees_owed_a: 0 admin_fees_owed_b: 0.05 - + - CONFIG'S ADDRESS + ACME & - USDC MINTS + "AUTHORITY" - pool authority - an address with no account - - - - POOL AUTHORITY + - TOKEN PROGRAM + ACME MINT - - - pool_a: the pool's ACME - - ≈ 459.3 ACME - authority: the pool authority + POOL CONFIG + + TOKEN PROGRAM + ACME MINT + + + pool_a: the pool's ACME + + ≈ 459.3 ACME + authority: the pool config - - POOL AUTHORITY + - TOKEN PROGRAM + USDC MINT - - - pool_b: the pool's USDC - - 1,225.00 USDC - authority: the pool authority + + POOL CONFIG + + TOKEN PROGRAM + USDC MINT + + + pool_b: the pool's USDC + + 1,225.00 USDC + authority: the pool config - - CONFIG'S ADDRESS + ACME & - USDC MINTS + "LIQUIDITY" - - - LP mint + + CONFIG'S ADDRESS + ACME & + USDC MINTS + "LIQUIDITY" + + + LP mint - + swap_tokens() · 100 USDC $0.30 of that is the fee (30 bps): $0.05 accrues to Maria, the LPs' $0.25 simply stays in the vault 99.70 is the taxed input the curve prices off - + ≈ 40.7 ACME - signed by the pool authority + signed by the pool config diff --git a/building-financial-products-in-solana/figures/amm-first-deposit.svg b/building-financial-products-in-solana/figures/amm-first-deposit.svg index 66549b46..ca5520df 100644 --- a/building-financial-products-in-solana/figures/amm-first-deposit.svg +++ b/building-financial-products-in-solana/figures/amm-first-deposit.svg @@ -1,4 +1,4 @@ - + Maria seeds the pool @@ -163,52 +163,45 @@ admin_fees_owed_a: 0 admin_fees_owed_b: 0 - + - CONFIG'S ADDRESS + ACME & - USDC MINTS + "AUTHORITY" - pool authority - an address with no account - - - - POOL AUTHORITY + - TOKEN PROGRAM + ACME MINT - - - pool_a: the pool's ACME - - 400 ACME - authority: the pool authority + POOL CONFIG + + TOKEN PROGRAM + ACME MINT + + + pool_a: the pool's ACME + + 400 ACME + authority: the pool config - - POOL AUTHORITY + - TOKEN PROGRAM + USDC MINT - - - pool_b: the pool's USDC - - 900 USDC - authority: the pool authority + + POOL CONFIG + + TOKEN PROGRAM + USDC MINT + + + pool_b: the pool's USDC + + 900 USDC + authority: the pool config - - CONFIG'S ADDRESS + ACME & - USDC MINTS + "LIQUIDITY" - - - LP mint - - supply: 599.9999 LP - authority: the pool authority + + CONFIG'S ADDRESS + ACME & + USDC MINTS + "LIQUIDITY" + + + LP mint + + supply: 599.9999 LP + authority: the pool config - + - + - + the first deposit sets the price: the ratio she deposits is the ratio, 900 ÷ 400 = $2.25 diff --git a/building-financial-products-in-solana/figures/options-account-map.svg b/building-financial-products-in-solana/figures/options-account-map.svg index 44e5c975..1d9c2494 100644 --- a/building-financial-products-in-solana/figures/options-account-map.svg +++ b/building-financial-products-in-solana/figures/options-account-map.svg @@ -1,4 +1,4 @@ - + Every account the venue touches @@ -138,36 +138,28 @@ fee_bps: 100 - - "AUTHORITY" + + + "UNDERLYING_VAULT" + MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults + + + + underlying vault + + 5 NVDAx + authority: the market - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault - - 5 NVDAx - authority: the market authority - - - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault - - 0 USDC - authority: the market authority + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault + + 0 USDC + authority: the market every token in either vault is owed to a named writer or holder, or to the admin as a fee; the market's three counters say which diff --git a/building-financial-products-in-solana/figures/options-buy-call.svg b/building-financial-products-in-solana/figures/options-buy-call.svg index f397229e..fc486df2 100644 --- a/building-financial-products-in-solana/figures/options-buy-call.svg +++ b/building-financial-products-in-solana/figures/options-buy-call.svg @@ -1,4 +1,4 @@ - + Bob buys the lot @@ -134,39 +134,31 @@ fee_bps: 100 - - "AUTHORITY" + + + "UNDERLYING_VAULT" + MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults - - - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault + + + + underlying vault - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault - - 0.25 USDC - authority: the market authority + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault + + 0.25 USDC + authority: the market 1. buy_option() 24.75 USDC - + 2. Maria's fee · 0.25 USDC diff --git a/building-financial-products-in-solana/figures/options-buy-put.svg b/building-financial-products-in-solana/figures/options-buy-put.svg index 81f7f102..fe02e860 100644 --- a/building-financial-products-in-solana/figures/options-buy-put.svg +++ b/building-financial-products-in-solana/figures/options-buy-put.svg @@ -1,4 +1,4 @@ - + Dave buys the puts @@ -92,15 +92,15 @@ Dave's NVDAx - - DAVE'S ADDRESS + - TOKEN PROGRAM + - USDC MINT - - - Dave's USDC - - 980 USDC + + DAVE'S ADDRESS + + TOKEN PROGRAM + + USDC MINT + + + Dave's USDC + + 980 USDC @@ -134,39 +134,31 @@ fee_bps: 100 - - "AUTHORITY" + + + "UNDERLYING_VAULT" + MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults - - - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault + + + + underlying vault - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault - - 750.45 USDC - authority: the market authority + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault + + 750.45 USDC + authority: the market - - 1. buy_option() - 19.80 USDC - - + + 1. buy_option() + 19.80 USDC + + 2. Maria's fee · 0.20 USDC diff --git a/building-financial-products-in-solana/figures/options-collect-fees.svg b/building-financial-products-in-solana/figures/options-collect-fees.svg index b81a32fb..9ef7aea5 100644 --- a/building-financial-products-in-solana/figures/options-collect-fees.svg +++ b/building-financial-products-in-solana/figures/options-collect-fees.svg @@ -1,4 +1,4 @@ - + Maria sweeps the venue's fees @@ -120,36 +120,28 @@ fees_owed: 0 USDC fee_bps: 100 - - - "AUTHORITY" + - MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults - - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault + + "UNDERLYING_VAULT" + + MARKET'S ADDRESS + + + + underlying vault - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault - - 0 USDC - authority: the market authority + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault + + 0 USDC + authority: the market - + 1. collect_fees() · 0.45 USDC diff --git a/building-financial-products-in-solana/figures/options-collect-proceeds.svg b/building-financial-products-in-solana/figures/options-collect-proceeds.svg index b240fc86..43469325 100644 --- a/building-financial-products-in-solana/figures/options-collect-proceeds.svg +++ b/building-financial-products-in-solana/figures/options-collect-proceeds.svg @@ -1,4 +1,4 @@ - + Alice collects the strike @@ -143,36 +143,28 @@ fees_owed: 0.25 USDC fee_bps: 100 - - - "AUTHORITY" + - MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults - - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault + + "UNDERLYING_VAULT" + + MARKET'S ADDRESS + + + + underlying vault - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault - - 0.25 USDC - authority: the market authority - - + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault + + 0.25 USDC + authority: the market + + 1. Alice collects 900 USDC diff --git a/building-financial-products-in-solana/figures/options-exercise.svg b/building-financial-products-in-solana/figures/options-exercise.svg index 85e8eaf3..7bc7f674 100644 --- a/building-financial-products-in-solana/figures/options-exercise.svg +++ b/building-financial-products-in-solana/figures/options-exercise.svg @@ -1,4 +1,4 @@ - + Bob exercises the five calls @@ -144,41 +144,33 @@ fee_bps: 100 - - "AUTHORITY" + + + "UNDERLYING_VAULT" + MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults + + + + underlying vault + + 0 NVDAx + authority: the market - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault - - 0 NVDAx - authority: the market authority - - - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault - - 900.25 USDC - authority: the market authority - - + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault + + 900.25 USDC + authority: the market + + 1. exercise_option() · 900 USDC - + 2. Bob receives 5 NVDAx diff --git a/building-financial-products-in-solana/figures/options-reclaim.svg b/building-financial-products-in-solana/figures/options-reclaim.svg index 3115b80e..9bb1459a 100644 --- a/building-financial-products-in-solana/figures/options-reclaim.svg +++ b/building-financial-products-in-solana/figures/options-reclaim.svg @@ -1,4 +1,4 @@ - + Carol reclaims her collateral @@ -134,36 +134,28 @@ fees_owed: 0.45 USDC fee_bps: 100 - - - "AUTHORITY" + - MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults - - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault + + "UNDERLYING_VAULT" + + MARKET'S ADDRESS + + + + underlying vault - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault - - 0.45 USDC - authority: the market authority + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault + + 0.45 USDC + authority: the market - + 1. Carol reclaims 750 USDC diff --git a/building-financial-products-in-solana/figures/options-write-call.svg b/building-financial-products-in-solana/figures/options-write-call.svg index 2759c15c..50c11a15 100644 --- a/building-financial-products-in-solana/figures/options-write-call.svg +++ b/building-financial-products-in-solana/figures/options-write-call.svg @@ -1,4 +1,4 @@ - + Alice writes five covered calls @@ -132,36 +132,28 @@ fees_owed: 0 USDC fee_bps: 100 - - - "AUTHORITY" + - MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults - - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault - - 5 NVDAx - authority: the market authority + + "UNDERLYING_VAULT" + + MARKET'S ADDRESS + + + + underlying vault + + 5 NVDAx + authority: the market - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault - + 1. write_option() 5 NVDAx diff --git a/building-financial-products-in-solana/figures/options-write-put.svg b/building-financial-products-in-solana/figures/options-write-put.svg index c8b4d2f6..8d4fc09e 100644 --- a/building-financial-products-in-solana/figures/options-write-put.svg +++ b/building-financial-products-in-solana/figures/options-write-put.svg @@ -1,4 +1,4 @@ - + Carol writes five cash-secured puts @@ -133,35 +133,27 @@ fee_bps: 100 - - "AUTHORITY" + + + "UNDERLYING_VAULT" + MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults - - - - "UNDERLYING_VAULT" + - MARKET'S ADDRESS - - - - underlying vault + + + + underlying vault - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - - quote vault - - 750.25 USDC - authority: the market authority + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + + quote vault + + 750.25 USDC + authority: the market - + 1. write_option() 750 USDC diff --git a/building-financial-products-in-solana/figures/perps-account-map.svg b/building-financial-products-in-solana/figures/perps-account-map.svg index 1c31b14d..75f8b74a 100644 --- a/building-financial-products-in-solana/figures/perps-account-map.svg +++ b/building-financial-products-in-solana/figures/perps-account-map.svg @@ -1,4 +1,4 @@ - + Every account a position touches Solana Devnet @@ -178,32 +178,24 @@ short_size: 5,000 cumulative_funding: 0 - + - "AUTHORITY" + POOL - - - pool authority + "VAULT" + POOL + + + custody vault + 14,000 USDC + authority: the pool - - "VAULT" + POOL - - - custody vault - - 14,000 USDC - authority: the pool authority - - - - "LP_MINT" + POOL - - - LP share mint - - mint authority: + + "LP_MINT" + POOL + + + LP share mint + + mint authority: the pool diff --git a/building-financial-products-in-solana/figures/perps-liquidation.svg b/building-financial-products-in-solana/figures/perps-liquidation.svg index c9d6aa21..20a695a0 100644 --- a/building-financial-products-in-solana/figures/perps-liquidation.svg +++ b/building-financial-products-in-solana/figures/perps-liquidation.svg @@ -1,4 +1,4 @@ - + Bob is liquidated at $115 @@ -160,30 +160,22 @@ short_size: 0 cumulative_funding: 0 - + - "AUTHORITY" + POOL - - - pool authority + "VAULT" + POOL + + + custody vault - - - - "VAULT" + POOL - - - custody vault - - 13,755 USDC - authority: the pool authority + 13,755 USDC + authority: the pool - - "LP_MINT" + POOL - - - LP share mint + + "LP_MINT" + POOL + + + LP share mint @@ -192,8 +184,8 @@ NVDAx: $115 - - + + 2. Bob is refunded 195 USDC his equity, less the liquidator's fee 3. Dave takes 50 USDC diff --git a/building-financial-products-in-solana/figures/perps-open-position.svg b/building-financial-products-in-solana/figures/perps-open-position.svg index 001e68dd..0870cc71 100644 --- a/building-financial-products-in-solana/figures/perps-open-position.svg +++ b/building-financial-products-in-solana/figures/perps-open-position.svg @@ -1,4 +1,4 @@ - + Alice opens a long @@ -140,45 +140,22 @@ liquidate_position() collect_fees() - - - "POOL" + COLLATERAL MINT + - ORACLE FEED ADDRESS - - - Pool - - liquidity: 12,000 USDC - reserved_liquidity: 5,000 - total_collateral: 995 - protocol_fees: 5 - long_size: 5,000 - short_size: 0 - cumulative_funding: 0 - - + - "AUTHORITY" + POOL - - - pool authority - - - - "VAULT" + POOL - - - custody vault - - 13,000 USDC - authority: the pool authority + "VAULT" + POOL + + + custody vault + + 13,000 USDC + authority: the pool - - "LP_MINT" + POOL - - - LP share mint + + "LP_MINT" + POOL + + + LP share mint @@ -187,7 +164,7 @@ NVDAx: $100 - + 1,000 USDC collateral in open_position() · side Long, 5,000 size diff --git a/building-financial-products-in-solana/figures/prop-amm-account-map.svg b/building-financial-products-in-solana/figures/prop-amm-account-map.svg index 272ef801..9ab14502 100644 --- a/building-financial-products-in-solana/figures/prop-amm-account-map.svg +++ b/building-financial-products-in-solana/figures/prop-amm-account-map.svg @@ -1,4 +1,4 @@ - + Every account the quote touches @@ -122,36 +122,27 @@ max_confidence_bps: 100 paused: false - - + - "AUTHORITY" + + "BASE_VAULT" + MARKET'S ADDRESS - market authority - an address with no account; - it exists only to sign for the vaults - - - - "BASE_VAULT" + - MARKET'S ADDRESS - - - base vault - - 1,000 NVDAx - authority: the market authority + + + base vault + + 1,000 NVDAx + authority: the market - - "QUOTE_VAULT" + - MARKET'S ADDRESS - - - quote vault - - 200,000 USDC - authority: the market authority + + "QUOTE_VAULT" + + MARKET'S ADDRESS + + + quote vault + + 200,000 USDC + authority: the market diff --git a/building-financial-products-in-solana/options.md b/building-financial-products-in-solana/options.md index 17bd2a90..d6a1405f 100644 --- a/building-financial-products-in-solana/options.md +++ b/building-financial-products-in-solana/options.md @@ -53,7 +53,7 @@ The **market** is a PDA with seeds `"market"`, the underlying mint, and the quot - `quote_locked`: how much of the quote vault the market owes, as put writers' collateral and as call holders' strike payments waiting to be collected. - `fees_owed`: how much of the quote vault belongs to the admin. -A dataless **market authority** PDA (seeds `"authority"` + market) owns the **underlying vault** and the **quote vault** (seeds `"underlying_vault"` and `"quote_vault"` + market) and exists to sign for them. Both vaults hold other people's money, mixed: the underlying vault holds every call writer's shares together, and the quote vault holds every put writer's cash, every exercised call's strike payment, and the venue's fees together. One rule keeps that safe: a vault balance is custody, and the three counters are the truth about who is owed what. +The market owns the **underlying vault** and the **quote vault** (seeds `"underlying_vault"` and `"quote_vault"` + market) and, being a PDA, signs for them with its own seeds. Both vaults hold other people's money, mixed: the underlying vault holds every call writer's shares together, and the quote vault holds every put writer's cash, every exercised call's strike payment, and the venue's fees together. One rule keeps that safe: a vault balance is custody, and the three counters are the truth about who is owed what. Each option is an `OptionContract`, a PDA with seeds `"option"`, the market, the writer, and an `id` the writer chooses, so one writer can have many options open. Its fields are the five terms plus the bookkeeping around them: diff --git a/building-financial-products-in-solana/perpetual-futures.md b/building-financial-products-in-solana/perpetual-futures.md index 58e3f266..440a37a8 100644 --- a/building-financial-products-in-solana/perpetual-futures.md +++ b/building-financial-products-in-solana/perpetual-futures.md @@ -14,7 +14,7 @@ That design decision creates the problem the program is organized around: the po ## The Account Model: One Pool, One Vault, One Position per Side per Trader -![The account map: the pool account and its authority PDA, the single custody vault and LP share mint they control, one position account per trader per side, and the oracle feed fixed at the pool's creation.](figures/perps-account-map.svg) +![The account map: the pool account, the single custody vault and LP share mint it controls, one position account per trader per side, and the oracle feed fixed at the pool's creation.](figures/perps-account-map.svg) The **pool** is a PDA with seeds `"pool"`, the collateral mint, and the oracle feed, so one market exists per collateral-and-feed pair. Its struct, `Pool`, carries the risk parameters set at creation (`max_leverage`, `maintenance_margin_bps`, `open_fee_bps`, `close_fee_bps`, `liquidation_fee_bps`, `max_confidence_bps`, `funding_rate_per_slot`) and the live accounting this chapter keeps returning to: @@ -25,7 +25,7 @@ The **pool** is a PDA with seeds `"pool"`, the collateral mint, and the oracle f - `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_slot`: the funding index. -A **pool authority** PDA (seeds `"authority"` + pool) holds no data and exists to sign. It 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. 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 *Token Fundraiser* chapter's: balances are custody, state is truth. Each position is a **position** PDA with seeds `"position"`, the pool, the owner, and the side (`"long"` or `"short"`), so a trader can hold at most one long and one short per market. The `Position` struct is a snapshot taken at open: `collateral` (net of the open fee), `size` (the notional exposure), `entry_price`, `size_scaled` (size divided by entry price, kept at `SIZE_PRECISION` fixed point), and `entry_funding` (the funding index at entry). Everything the program ever needs to settle the position is derivable from these fields plus the current price, which is what makes settlement **account-local**: closing or liquidating one position touches one position. @@ -84,7 +84,7 @@ The example documents two simplifications: Production funding is proportional to the imbalance and flows between the sides; the mechanism here teaches the accounting structure, an index plus per-position entry snapshots, which is the part that survives into every real implementation. The test `test_funding_charged_to_long` opens a lone long, warps two thousand slots, and asserts the pool collected exactly the funding the index accrued. To keep every other number in this chapter clean, the walkthrough pool sets the funding rate to zero, as the test suite's default market does. -A flat rate is still a per-slot rate. The rate is quoted per slot, so what a position costs per hour follows the cluster's slot time as well as the rate. A pool that outlives a shortening of the slot charges the heavier side more per hour than it was set up to. `set_funding_rate()` lets the pool authority bring it back in line, advancing the index at the old rate before storing the new one so the slots already elapsed are charged at the rate that applied to them. The *Time* chapter covers why any rate counted in slots needs that. +A flat rate is still a per-slot rate. The rate is quoted per slot, so what a position costs per hour follows the cluster's slot time as well as the rate. A pool that outlives a shortening of the slot charges the heavier side more per hour than it was set up to. `set_funding_rate()` lets the pool operator bring it back in line, advancing the index at the old rate before storing the new one so the slots already elapsed are charged at the rate that applied to them. The *Time* chapter covers why any rate counted in slots needs that. ![The funding index climbs while longs outweigh shorts and falls when shorts outweigh longs; each position pays or receives the index movement between its entry and its exit.](figures/perps-funding-index.svg) @@ -105,7 +105,7 @@ The pool cannot sell Bob's position; a program never acts on its own, somebody h ## Closing at $120, and the Pool Pays From Its Reservation -At $120 Alice takes her win. `close_position()` marks her position one last time: profit 5,000 × 20 ÷ 100 = 1,000 USDC, under her 5,000 cap; close fee 10 basis points of size = 5; payout 995 + 1,000 − 5 = **1,990 USDC**. That nearly doubles her stake on a 20% move, which is what 5x leverage means. Her 5,000 reservation releases, the LPs' `liquidity` drops by her 1,000 profit, `protocol_fees` collects the 5, her position account closes, and the vault pays her out under the pool authority's signature. The same handler protects the pool from the other direction. A position whose equity cannot cover its own close fee is refused with `PositionNotHealthy` and must exit through liquidation, and any payout the pool arithmetic cannot cover is refused with `PoolInsolvent` rather than overdrawing the LPs. +At $120 Alice takes her win. `close_position()` marks her position one last time: profit 5,000 × 20 ÷ 100 = 1,000 USDC, under her 5,000 cap; close fee 10 basis points of size = 5; payout 995 + 1,000 − 5 = **1,990 USDC**. That nearly doubles her stake on a 20% move, which is what 5x leverage means. Her 5,000 reservation releases, the LPs' `liquidity` drops by her 1,000 profit, `protocol_fees` collects the 5, her position account closes, and the vault pays her out under the pool's signature. The same handler protects the pool from the other direction. A position whose equity cannot cover its own close fee is refused with `PositionNotHealthy` and must exit through liquidation, and any payout the pool arithmetic cannot cover is refused with `PoolInsolvent` rather than overdrawing the LPs. Everyone ends the story with what their position promised: diff --git a/building-financial-products-in-solana/prop-amm.md b/building-financial-products-in-solana/prop-amm.md index c7618470..238ebdc8 100644 --- a/building-financial-products-in-solana/prop-amm.md +++ b/building-financial-products-in-solana/prop-amm.md @@ -15,9 +15,9 @@ Two things are absent as a result: ## The Account Model: One Market, Two Vaults, and Nothing Else -![The authority PDA holds no data at all; it exists only to sign for the two vaults.](figures/prop-amm-account-map.svg) +![The market account owns the two vaults and signs for them; the oracle feed is an account the program only reads.](figures/prop-amm-account-map.svg) -The **market** is 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. A dataless **market authority** PDA owns the **base vault** and **quote vault**, and exists to sign. +The **market** is 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 owns the **base vault** and **quote vault** and, being a PDA, signs for them. 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 lands in the inventory itself. 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 the entire accounting.