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
2 changes: 2 additions & 0 deletions .github/workflows/kani.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ jobs:
- betting-market
- vault-strategy
- token-fundraiser
- prop-amm
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
Expand All @@ -86,6 +87,7 @@ jobs:
- betting-market
- vault-strategy
- token-fundraiser
- prop-amm
steps:
- uses: actions/checkout@v5
- name: Run Kani
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ An exchange with no order book: swaps fill instantly against a shared liquidity

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

### Prop AMM

A **proprietary AMM**: a market-making firm funds a venue with its own capital and quotes both sides of it, selling the base token at the oracle price plus a spread and buying it back at the oracle price minus the spread. No pricing curve, no liquidity providers, no pool shares — the operator is the only capital in the market, can re-quote or pull its quotes at will, and earns the spread instead of a fee. Because the price comes from an oracle rather than the pool's balances, trades have no price impact and nothing to sandwich. This is the design behind venues like Lifinity, SolFi, and HumidiFi, which fill most Solana swap volume through Jupiter routing.

[⚓ Anchor](./finance/prop-amm/anchor) [💫 Quasar](./finance/prop-amm/quasar)

### Vault Strategy

A managed investment fund onchain, like an ETF or mutual fund. Investors deposit USDC for shares, a manager allocates the pool across a basket of assets (here, stocks like TSLAx and NVDAx), and each share's value tracks the fund's net asset value. The manager earns a management fee, and investors redeem a proportional slice of the underlying assets.
Expand Down
6 changes: 6 additions & 0 deletions finance/prop-amm/anchor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.anchor
target
**/*.rs.bk
node_modules
test-ledger
.DS_Store
26 changes: 26 additions & 0 deletions finance/prop-amm/anchor/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[toolchain]
# Match the repo package manager (pnpm-lock.yaml at root); avoids Anchor's yarn default.
package_manager = "pnpm"
solana_version = "3.1.8"

[features]
resolution = true
skip-lint = false

[programs.localnet]
prop_amm = "9ZMtJFtn5n4wwpEeXXG5paFQakcDtrd3ova5ptJL4VT1"
mock_switchboard = "BdFYarCfgMJhC26dFN6JXKVx4kUEM3k2wYaNjyjFrvAD"

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

[scripts]
test = "cargo test"

# Non-default: the LiteSVM Rust tests load both programs' .so files, so they
# must be built before `cargo test` runs. CI calls `anchor build` first; these
# waits only matter for the legacy validator path.
[test]
startup_wait = 5000
shutdown_wait = 2000
8 changes: 8 additions & 0 deletions finance/prop-amm/anchor/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## 2026-07-11

Initial version: an oracle-quoted proprietary AMM. One operator funds the
market's inventory and quotes both sides of it at the oracle price plus a
spread; anyone can swap against the quotes. Includes the `mock-switchboard`
oracle program for deterministic tests.
14 changes: 14 additions & 0 deletions finance/prop-amm/anchor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[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
156 changes: 156 additions & 0 deletions finance/prop-amm/anchor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Prop AMM

An oracle-quoted **proprietary AMM**: a market-making firm funds a trading
venue with its own capital and quotes both sides of it — anyone can buy the
base token at the oracle price plus a spread, or sell at the oracle price
minus it. There is no pricing curve, there are no liquidity providers, and
there are no pool shares: the operator is the only capital in the market,
which is the property that gives the design its name. This is the
architecture behind venues like Lifinity (the pioneer), SolFi, HumidiFi,
ZeroFi, Tessera, and Obric, which collectively fill most Solana swap volume
via Jupiter routing rather than their own user interfaces.

## Programs

- **`prop-amm`** — the market: one operator, one base/quote pair, one oracle
feed, two vaults, five instruction handlers.
- **`mock-switchboard`** — a minimal stand-in for a Switchboard On-Demand
price feed, so tests can drive deterministic price scenarios. Not for
production.

## Key Financial Concepts

### Proprietary market making

A constant-product AMM crowdsources its capital from liquidity providers and
pays them fees; its price is a function of its reserves. A prop AMM inverts
all of it: the firm quotes prices taken from an oracle, earns the spread
instead of a fee, and risks only its own inventory. Because the price does
not depend on the pool's balances, big trades pay the same unit price as
small ones (no price impact), and there is nothing for a sandwich attacker to
squeeze — the classic front-run/back-run pattern needs a price that moves
with each trade.

### The quote: oracle, spread, bid and ask

The market pins an oracle feed at creation. Every swap reads the current
price and applies the operator's `spread_bps` each way: buyers of the base
token pay the **ask** (oracle plus spread, rounded up), sellers receive the
**bid** (oracle minus spread, rounded down). Output amounts floor. Every
rounding direction favors the market, and after the math the handler asserts
the invariant those roundings guarantee: the value leaving the vaults, at the
raw oracle price, never exceeds the value coming in.

### Inventory, not liquidity

The vault balances are not a pricing input; they only bound what the market
can deliver. A swap bigger than the inventory is rejected whole
(`InsufficientInventory`) rather than partially filled or mispriced. The
operator deposits and withdraws inventory freely — including all of it, at any
time. Nobody else has a claim on the vaults, so there is no share mint, no
pro-rata withdrawal math, and no inflation attack surface: the empty-pool
games that plague shared pools need shares to dilute, and there are none.

### Adverse selection and pulled quotes

A market maker's enemy is informed flow: traders who know the price is about
to move and hit the stale side of the quote. The two defenses this program
ships are the oracle gates (below) and `set_quote`, which lets the operator
widen the spread or pause quoting entirely. Real prop AMMs do exactly this —
during fast markets their quotes vanish and return minutes later.

### Oracle staleness and confidence

Every swap re-validates the feed: the price must be positive, at the pinned
scale, no older than 150 slots (~1 minute), and its confidence band must be
inside `max_confidence_bps`. For this design the staleness bound is not
hygiene, it is the business: a quote priced off an old number is a free
option for whoever notices first.

## Program Flow

### Participants

- **Maria** operates the market-making firm.
- **Alice** and **Bob** trade NVDAx (tokenized NVIDIA stock, 6 decimals)
against USDC.
- The oracle quotes NVDAx at **$165** with 8 decimals of scale.

### Step 1: Maria opens the market

`initialize_market` creates the `Market` account (PDA of the mint pair), a
dataless vault-authority PDA, and the two vaults, and pins the oracle feed,
its scale, a 10 bps spread, and a 1% confidence limit. One market per pair:
the deployment is the firm.

### Step 2: Maria stocks the inventory

`deposit_inventory` moves 1,000 NVDAx and 200,000 USDC of the firm's own
tokens into the vaults. No shares are minted to anyone, because there is
nobody else to account for.

### Step 3: Alice buys 10 NVDAx at the ask

At $165 with a 10 bps spread the ask is $165.165. Alice's `swap`
(`Direction::BuyBase`) spends exactly 1,651.65 USDC for 10 NVDAx —
whether she bought 1 or 500, the unit price would be the same.

### Step 4: Bob sells 10 NVDAx at the bid

The bid is $164.835, so Bob's `swap` (`Direction::SellBase`) receives
exactly 1,648.35 USDC. A round trip through both sides costs exactly the
3.30 USDC spread — the spread is the fee, and it lands in the inventory,
not in a fee ledger.

### Step 5: The oracle reprices; the quote follows

The feed moves to $170 and the very next swap prices at $170.17 ask. No
arbitrageur had to walk the price there trade by trade, which is how a curve
AMM gets from one price to another.

### Step 6: Volatility: Maria widens the spread, then pulls quotes

`set_quote(50, false)` re-prices the market at a 50 bps spread;
`set_quote(50, true)` pauses it entirely, and swaps fail with
`MarketPaused` until she returns.

### Step 7: Maria withdraws her inventory

`withdraw_inventory` returns every token in both vaults to the firm. The
market still exists but rejects fills — an empty prop AMM refuses rather than
misprices.

## Design notes and further reading

- Production prop AMMs on Solana are closed-source and considerably more
sophisticated: they blend multiple price sources, run inventory-skewed
quoting (shading the quote to reduce a lopsided inventory), and integrate
with aggregators via quote APIs. The skeleton — operator capital, oracle
price, spread, hard oracle gates — is this program.
- Lifinity's public design notes and the Helius write-up
"Solana's Proprietary AMM Revolution" are good next reads.
- The oracle reader deliberately reads raw bytes at fixed offsets and
documents how to swap in `switchboard_on_demand::PullFeedAccountData::
parse_and_verify(...)` for production.

## Limitations

- The oracle feed's owning program is not verified — the operator picks the
feed, and a bad choice loses the operator's money, not the traders'. A
production reader must still check the account owner.
- One flat spread both ways; no inventory skew, no size-dependent pricing.
- `paused` is the only circuit breaker; production venues also bound
per-slot volume and single-fill size.

## Testing

```bash
anchor build
cargo test
```

The LiteSVM suite (`programs/prop-amm/tests/test_prop_amm.rs`) verifies the
quote math to the minor unit in both directions, the exact round-trip spread,
oracle repricing and re-quoting, and that every gate shuts: slippage,
staleness, confidence, pause, zero amounts, inventory bounds, and operator
access control.
26 changes: 26 additions & 0 deletions finance/prop-amm/anchor/programs/mock-switchboard/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "mock_switchboard"
version = "0.1.0"
description = "Mock Switchboard On-Demand feed for testing the prop-amm program"
edition = "2021"

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

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

[dependencies]
anchor-lang = "1.1.2"

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
105 changes: 105 additions & 0 deletions finance/prop-amm/anchor/programs/mock-switchboard/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! Mock Switchboard On-Demand feed for testing the prop-amm program.
//!
//! Real Switchboard On-Demand feeds are program-owned accounts whose data is
//! produced by an offchain oracle network and verified onchain via Ed25519
//! signatures over the latest price update. That verification path is
//! out-of-scope for this teaching example, so this mock stores a single price
//! the test harness writes directly, plus the slot the update happened in.
//!
//! The prop-amm program reads this feed the same way it would read a real
//! feed: load the account, decode the layout, read `price`, `scale`, and
//! `last_update_slot` (see `prop_amm::state::oracle`). Swap this program ID
//! for `SBondMDrcV3K4kxZR1HNVT7osZxAHVHgYXL5Ze1oMUv` (Switchboard On-Demand)
//! and adapt the layout to consume real feeds in production.
//!
//! NOT FOR PRODUCTION.
use anchor_lang::prelude::*;

declare_id!("BdFYarCfgMJhC26dFN6JXKVx4kUEM3k2wYaNjyjFrvAD");

#[program]
pub mod mock_switchboard {
use super::*;

/// Initialize the mock feed with an initial price. The signer becomes the
/// authority allowed to push later price updates.
pub fn initialize_feed(
context: Context<InitializeFeedAccountConstraints>,
price: i128,
scale: u32,
confidence: u64,
) -> Result<()> {
let feed = &mut context.accounts.feed;
feed.authority = context.accounts.authority.key();
feed.price = price;
feed.scale = scale;
feed.last_update_slot = Clock::get()?.slot;
feed.confidence = confidence;
Ok(())
}

/// Push a new price (and confidence band) to the mock feed. In real
/// Switchboard this would be a signed update from the oracle network; here it
/// is an authority-gated write, because the goal is to drive deterministic
/// test scenarios.
pub fn set_price(
context: Context<SetPriceAccountConstraints>,
price: i128,
confidence: u64,
) -> Result<()> {
let feed = &mut context.accounts.feed;
feed.price = price;
feed.last_update_slot = Clock::get()?.slot;
feed.confidence = confidence;
Ok(())
}
}

#[derive(Accounts)]
pub struct InitializeFeedAccountConstraints<'info> {
#[account(
init,
payer = authority,
space = MockFeed::DISCRIMINATOR.len() + MockFeed::INIT_SPACE,
)]
pub feed: Account<'info, MockFeed>,

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

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

#[derive(Accounts)]
pub struct SetPriceAccountConstraints<'info> {
#[account(
mut,
has_one = authority,
)]
pub feed: Account<'info, MockFeed>,

pub authority: Signer<'info>,
}

/// Mock of a Switchboard On-Demand feed. Real feeds carry many more fields
/// (median, range, sample window, signatures) — this is the bare minimum the
/// prop-amm program needs to price a quote.
#[derive(InitSpace)]
#[account]
pub struct MockFeed {
pub authority: Pubkey,

/// Signed 128-bit fixed-point price. Real Switchboard prices are also i128.
pub price: i128,

/// Number of decimal places implied by `price`. E.g. `scale = 8` means
/// `price = 200 * 10^8` represents $200.00000000.
pub scale: u32,

pub last_update_slot: u64,

/// Uncertainty band around `price`, in the same fixed point. Real feeds
/// report a standard-deviation-like confidence; consumers reject the price
/// when this is too wide relative to `price`.
pub confidence: u64,
}
Loading
Loading