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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ node_modules/

/target
deploy
.claude
.claude/*
# Exception: skills are shared team resources
!.claude/skills/
!.claude/skills/**
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Constant product AMM (x·y=k) — create liquidity pools, deposit and withdraw l

[⚓ Anchor](./tokens/token-swap/anchor) [💫 Quasar](./tokens/token-swap/quasar)

### Central Limit Order Book

Order-book exchange — users post limit bids and asks at chosen prices, tokens are locked in program vaults, and orders cross against the opposing side using price-time priority. Fees route to a dedicated fee vault, maker/taker proceeds land in unsettled balances, and funds are withdrawn via `settle_funds`. A minimal teaching example of the mechanics behind Openbook and Phoenix.

[⚓ Anchor](./defi/order-book/anchor)

### Escrow

Peer-to-peer OTC trade — one user deposits token A and specifies how much token B they want. A counterparty fulfills the offer and both sides receive their tokens atomically.
Expand Down
7 changes: 7 additions & 0 deletions defi/order-book/anchor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.anchor
.DS_Store
target
**/*.rs.bk
node_modules
test-ledger
.yarn
20 changes: 20 additions & 0 deletions defi/order-book/anchor/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[toolchain]
# Pin Solana to the version used across the repo's Anchor 1.0 examples so the
# bundled test validator and BPF toolchain stay in lock-step.
solana_version = "3.1.8"

[features]
resolution = true
skip-lint = false

[programs.localnet]
order_book = "C69UJ8irfmHq5ysyLek7FKApHR86FBeupiz4JnoyPzzx"

[provider]
cluster = "Localnet"
wallet = "~/.config/solana/id.json"

[scripts]
# LiteSVM Rust tests live under `programs/order-book/tests/` and include the built
# `.so` via `include_bytes!`, so a fresh `anchor build` must run first.
test = "cargo test"
13 changes: 13 additions & 0 deletions defi/order-book/anchor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[workspace]
members = ["programs/*"]
resolver = "2"

[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1

[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1
1,639 changes: 1,639 additions & 0 deletions defi/order-book/anchor/README.md

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions defi/order-book/anchor/programs/order-book/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "order_book"
version = "0.1.0"
description = "Order book example (CLOB) on Solana"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "order_book"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
anchor-debug = []
custom-heap = []
custom-panic = []

[dependencies]
anchor-lang = "1.0.0"
anchor-spl = "1.0.0"
# Used by the ported Openbook slab — `bytemuck::Pod` / `Zeroable` on every node
# variant + `min_const_generics` so `[AnyNode; 1024]` can derive Pod without
# hitting bytemuck's default-32 array cap. `static_assertions` keeps the slab
# layout asserts (node size, alignment) compile-time, matching upstream.
bytemuck = { version = "1.18", features = ["derive", "min_const_generics"] }
static_assertions = "1.1"

[dev-dependencies]
# Match the test stack used by tokens/escrow, defi/asset-leasing, and the
# other LiteSVM-based Anchor examples so contributors can move between them
# without version drift.
litesvm = "0.11.0"
solana-signer = "3.0.0"
solana-keypair = "3.0.1"
solana-kite = "0.3.0"

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
67 changes: 67 additions & 0 deletions defi/order-book/anchor/programs/order-book/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use anchor_lang::prelude::*;

#[error_code]
pub enum ErrorCode {
#[msg("Invalid price provided")]
InvalidPrice,

#[msg("Order not found")]
OrderNotFound,

#[msg("Market is currently paused")]
MarketPaused,

#[msg("Unauthorized action")]
Unauthorized,

#[msg("Order book is full")]
OrderBookFull,

#[msg("MarketUser has too many open orders")]
TooManyOpenOrders,

#[msg("Price does not align with tick size")]
InvalidTickSize,

#[msg("Quantity is below minimum order size")]
BelowMinOrderSize,

#[msg("Order is not cancellable in current status")]
OrderNotCancellable,

#[msg("Numerical overflow occurred")]
NumericalOverflow,

#[msg("Fee basis points out of range")]
InvalidFeeBasisPoints,

#[msg("Fee vault does not match the market's fee vault")]
InvalidFeeVault,

#[msg("Base vault does not match the market's base vault")]
InvalidBaseVault,

#[msg("Quote vault does not match the market's quote vault")]
InvalidQuoteVault,

#[msg("Base mint does not match the market's base mint")]
InvalidBaseMint,

#[msg("Quote mint does not match the market's quote mint")]
InvalidQuoteMint,

#[msg("Maker account provided does not correspond to a resting order on the book")]
MakerAccountMismatch,

#[msg("Not enough maker accounts supplied to cross the incoming order")]
MissingMakerAccounts,

#[msg("Maker order and maker MarketUser owner mismatch")]
MakerOwnerMismatch,

#[msg("Only the market authority can withdraw fees")]
NotMarketAuthority,

#[msg("Order book account does not match the market's order book")]
InvalidOrderBook,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use anchor_lang::prelude::*;

use crate::errors::ErrorCode;
use crate::state::{
remaining_quantity, remove_open_order, Market, Order, OrderBook, OrderSide, OrderStatus,
MarketUser, ORDER_SEED, MARKET_USER_SEED,
};

pub fn handle_cancel_order(context: Context<CancelOrder>) -> Result<()> {
let order = &mut context.accounts.order;

require!(
order.owner == context.accounts.owner.key(),
ErrorCode::Unauthorized
);

require!(
order.status == OrderStatus::Open || order.status == OrderStatus::PartiallyFilled,
ErrorCode::OrderNotCancellable
);

// Funds the order had locked in the vault are now owed back to the
// owner. Credit the appropriate unsettled balance; settle_funds moves
// those funds from the vault to the owner's token account.
let remaining = remaining_quantity(order);
if remaining > 0 {
let market_user = &mut context.accounts.market_user;
match order.side {
OrderSide::Bid => {
// u128 intermediate: the lock was originally taken on a
// u64 quote balance, so price * remaining must fit u64
// — but the multiplication itself can transiently exceed
// u64. Mirror the same pattern as place_order: widen,
// multiply, narrow.
let quote_amount: u64 = (order.price as u128)
.checked_mul(remaining as u128)
.ok_or(ErrorCode::NumericalOverflow)?
.try_into()
.map_err(|_| error!(ErrorCode::NumericalOverflow))?;
market_user.unsettled_quote = market_user
.unsettled_quote
.checked_add(quote_amount)
.ok_or(ErrorCode::NumericalOverflow)?;
}
OrderSide::Ask => {
market_user.unsettled_base = market_user
.unsettled_base
.checked_add(remaining)
.ok_or(ErrorCode::NumericalOverflow)?;
}
}
}

// Remove the leaf from the slab. The current cancel API doesn't tell us
// which side the order is on without reading the Order PDA — which we
// already have, so use it.
let mut order_book = context.accounts.order_book.load_mut()?;
let removed = order_book.remove_from(order.side, order.order_id).is_some();
require!(removed, ErrorCode::OrderNotFound);
drop(order_book);

let market_user = &mut context.accounts.market_user;
remove_open_order(market_user, order.order_id);

order.status = OrderStatus::Cancelled;

Ok(())
}

#[derive(Accounts)]
pub struct CancelOrder<'info> {
#[account(has_one = order_book @ ErrorCode::InvalidOrderBook)]
pub market: Account<'info, Market>,

// Not a PDA (see initialize_market.rs); bound to `market` via has_one.
#[account(mut)]
pub order_book: AccountLoader<'info, OrderBook>,

#[account(
mut,
seeds = [ORDER_SEED, market.key().as_ref(), order.order_id.to_le_bytes().as_ref()],
bump = order.bump
)]
pub order: Account<'info, Order>,

#[account(
mut,
seeds = [MARKET_USER_SEED, market.key().as_ref(), owner.key().as_ref()],
bump = market_user.bump
)]
pub market_user: Account<'info, MarketUser>,

pub owner: Signer<'info>,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use anchor_lang::prelude::*;

use crate::state::{Market, MarketUser, MARKET_USER_SEED};

pub fn handle_create_market_user(context: Context<CreateMarketUser>) -> Result<()> {
let market_user = &mut context.accounts.market_user;
market_user.market = context.accounts.market.key();
market_user.owner = context.accounts.owner.key();
market_user.unsettled_base = 0;
market_user.unsettled_quote = 0;
market_user.open_orders = Vec::new();
market_user.bump = context.bumps.market_user;

Ok(())
}

#[derive(Accounts)]
pub struct CreateMarketUser<'info> {
#[account(
init,
payer = owner,
space = MarketUser::DISCRIMINATOR.len() + MarketUser::INIT_SPACE,
seeds = [MARKET_USER_SEED, market.key().as_ref(), owner.key().as_ref()],
bump
)]
pub market_user: Account<'info, MarketUser>,

pub market: Account<'info, Market>,

#[account(mut)]
pub owner: Signer<'info>,

pub system_program: Program<'info, System>,
}
Loading
Loading