Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to this repository are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [2026-09-23] - The token fundraiser and order book Anchor v1 copies catch up

Under the old rule that `anchor-v1/` copies were frozen, two v1 copies were
left behind by changes to their v2 counterparts. Now that the copies track
each other, both are ported.

- Token fundraiser (Anchor v1): `close_contributor` and the
`FundraiserStillOpen` error, so a contributor to a successful raise can take
back their Contributor account's rent. Same two tests as the v2 copy.
- Order book (Anchor v1): the base, quote, and fee vaults are PDAs of the
market at `["base_vault", market]`, `["quote_vault", market]` and
`["fee_vault", market]`, so a client derives them instead of generating and
signing with three extra keys. The tests derive them too.

## [2026-09-23] - Anchor v1 copies track their Anchor v2 counterparts

CONTRIBUTING.md described each `anchor-v1/` copy as a frozen snapshot that
Expand Down
14 changes: 14 additions & 0 deletions finance/order-book/anchor-v1/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 2026-09-23

### Changed

- Ported from the Anchor v2 copy: the base, quote, and fee vaults are PDAs of
the market, at seeds `["base_vault", market]`, `["quote_vault", market]` and
`["fee_vault", market]`, instead of token accounts at public keys the client
generated. Clients derive the vault addresses instead of generating and
signing with three extra keys. The market still records each address and
every handler still checks the vaults it is passed against that record with
`has_one`. The order book stays a client-allocated account: at about 180 KB
it is too large for the program to create. Tests derive the vaults instead
of generating them.

## 2026-07-07

Added this changelog. Changes prior to this date were tracked in git history only.
42 changes: 23 additions & 19 deletions finance/order-book/anchor-v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ Maria's wallet signs. Five accounts are created:

- `Market` PDA: type Program data, seeds `["market", NVDAx_mint, USDC_mint]`, state after `fee_bps=25`, `tick_size=1`, `is_active=true`; vault addresses recorded
- `OrderBook`: type Zero-copy slab (~180 KB), seeds Client-allocated (not a PDA), state after Both critbit trees empty
- `base_vault`: type Token account (NVDAx), seeds Authority = Market PDA, state after 0 NVDAx
- `quote_vault`: type Token account (USDC), seeds Authority = Market PDA, state after 0 USDC
- `fee_vault`: type Token account (USDC), seeds Authority = Market PDA, state after 0 USDC
- `base_vault`: type Token account (NVDAx), seeds `["base_vault", market]`, authority Market PDA, state after 0 NVDAx
- `quote_vault`: type Token account (USDC), seeds `["quote_vault", market]`, authority Market PDA, state after 0 USDC
- `fee_vault`: type Token account (USDC), seeds `["fee_vault", market]`, authority Market PDA, state after 0 USDC

**No tokens move.** Maria pays the SOL rent for all five accounts.

Expand Down Expand Up @@ -372,23 +372,25 @@ Alice's remaining 2-NVDAx [bid](https://www.investopedia.com/terms/b/bid.asp) st
### State / data accounts

- `Market`: PDA yes, seeds `["market", base_mint, quote_mint]`, authority program, holds fee rate, tick size, min order size, base/quote mint pubkeys, vault pubkeys, order book pubkey, `authority` wallet (allowed to withdraw fees)
- `OrderBook`: PDA no (client-allocated keypair), seeds n/a: too large (~180 KB) for an `init`/CPI PDA, so created via `create_account` (which needs a signing key a PDA lacks); tied to its market via `has_one`; authority program, holds two critbit trees (bids highest-first, asks lowest-first, 1024 leaves each), `next_order_id`
- `OrderBook`: PDA no (client-allocated at a public key the client generates), seeds n/a: too large (~180 KB) for an `init`/CPI PDA, so created via `create_account` (which needs a signing key a PDA lacks); tied to its market via `has_one`; authority program, holds two critbit trees (bids highest-first, asks lowest-first, 1024 leaves each), `next_order_id`
- `Order`: PDA yes, seeds `["order", market, order_id.to_le_bytes()]`, authority program, holds owner, side, price, original_quantity, filled_quantity, status, timestamp
- `MarketUser`: PDA yes, seeds `["market_user", market, owner]`, authority program, holds `unsettled_base`, `unsettled_quote`, `open_orders: Vec<u64>` (max 20)

### Token accounts (owned by the Token Program, authority = Market PDA)

- `base_vault`: PDA no (regular token account), authority Market PDA, mint base, holds bids' locked base IS NOT STORED HERE - only asks' locked base sits here pre-match, plus base owed to bid-takers waiting for `settle_funds`
- `quote_vault`: PDA no, authority Market PDA, mint quote, holds bids' locked quote pre-match, plus quote owed to ask-takers and bid-makers waiting for settlement
- `fee_vault`: PDA no, authority Market PDA, mint quote, holds taker fees accumulated across all fills; drained by `withdraw_fees`
- `base_vault`: PDA yes, seeds `["base_vault", market]`, authority Market PDA, mint base, holds bids' locked base IS NOT STORED HERE - only asks' locked base sits here pre-match, plus base owed to bid-takers waiting for `settle_funds`
- `quote_vault`: PDA yes, seeds `["quote_vault", market]`, authority Market PDA, mint quote, holds bids' locked quote pre-match, plus quote owed to ask-takers and bid-makers waiting for settlement
- `fee_vault`: PDA yes, seeds `["fee_vault", market]`, authority Market PDA, mint quote, holds taker fees accumulated across all fills; drained by `withdraw_fees`

Note: the **token vaults are not PDAs**. They are regular token
accounts created with `init` in `initialize_market.rs`; their
*authority* is the Market PDA, so only the program can move funds out.
Their addresses are computed by the caller (e.g. generated Keypairs in
the tests) and then written to `market.base_vault` / `quote_vault` /
`fee_vault` for the program to validate them on later calls via
`has_one = fee_vault` etc.
Note: the **token vaults are PDAs of the market**, created with `init`
in `initialize_market.rs` at seeds `["base_vault", market]`,
`["quote_vault", market]` and `["fee_vault", market]`. Their *authority*
is the Market PDA, so only the program can move funds out. Any client can
derive them from the market's address. The market also records each
address, and later instruction handlers validate the vaults they are
passed with `has_one = fee_vault` etc., which is what stops the fee
vault being passed where the quote vault belongs. The order book is the
one market account that is not a PDA.

### Leaf layout in the `OrderBook` slab

Expand Down Expand Up @@ -543,7 +545,9 @@ pub fn initialize_market(
`#[account(zero)]`)
- `base_mint`, `quote_mint` (read-only)
- `base_vault`, `quote_vault`, `fee_vault` (all **init** as
`TokenAccount`s, authority = `market`)
`TokenAccount`s at seeds `["base_vault", market]`,
`["quote_vault", market]` and `["fee_vault", market]`,
authority = `market`)
- `token_program`, `system_program`

**Checks:**
Expand All @@ -559,10 +563,10 @@ the supplied parameters plus all the derived fields
(`market.authority`, the vault pubkeys, `is_active = true`,
`next_order_id = 1`).

The vaults are regular token accounts, *not* PDAs - their
addresses are chosen by the caller (typically fresh keypairs) and
captured on the market's state so later instruction handlers can
validate them.
The vaults are PDAs of the market, so the caller derives their
addresses rather than choosing them. The market's state records them
too, so later instruction handlers can validate the vaults they are
passed.

### 3.2 `initialize_market_user`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use anchor_lang::prelude::*;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};

use crate::errors::ErrorCode;
use crate::state::{Market, OrderBook, MARKET_SEED};
use crate::state::{
Market, OrderBook, BASE_VAULT_SEED, FEE_VAULT_SEED, MARKET_SEED, QUOTE_VAULT_SEED,
};

// Basis points are hundredths of a percent; 10000 bps == 100%. Fees above 100%
// would be nonsensical, so we cap here.
Expand Down Expand Up @@ -89,6 +91,8 @@ pub struct InitializeMarketAccountConstraints<'info> {
#[account(
init,
payer = authority,
seeds = [BASE_VAULT_SEED, market.key().as_ref()],
bump,
token::mint = base_mint,
token::authority = market,
token::token_program = token_program
Expand All @@ -98,6 +102,8 @@ pub struct InitializeMarketAccountConstraints<'info> {
#[account(
init,
payer = authority,
seeds = [QUOTE_VAULT_SEED, market.key().as_ref()],
bump,
token::mint = quote_mint,
token::authority = market,
token::token_program = token_program
Expand All @@ -109,6 +115,8 @@ pub struct InitializeMarketAccountConstraints<'info> {
#[account(
init,
payer = authority,
seeds = [FEE_VAULT_SEED, market.key().as_ref()],
bump,
token::mint = quote_mint,
token::authority = market,
token::token_program = token_program
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ use anchor_lang::prelude::*;

pub const MARKET_SEED: &[u8] = b"market";

// The three vaults are PDAs of the market: each is found from its own seed
// and the market's address, so any client can derive where the market keeps
// its tokens without reading the market first. The market also records each
// address, and every handler that touches a vault checks it against that
// record, which is what stops the fee vault being passed as the quote vault.
pub const BASE_VAULT_SEED: &[u8] = b"base_vault";
pub const QUOTE_VAULT_SEED: &[u8] = b"quote_vault";
pub const FEE_VAULT_SEED: &[u8] = b"fee_vault";

// A Market is one trading pair (base/quote) with its own vaults and order book.
// The market PDA itself is the authority of the token vaults, so funds can only
// move out via program-signed CPIs (place/cancel/settle).
Expand Down
Loading
Loading