From d70813017d819334415aecbe3b51a705088b4c7d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 14:12:11 +0000 Subject: [PATCH 1/5] docs: fix stale test commands, bankrun comments, and dead links - Replace `pnpm test` with `anchor test` in 42 Anchor example READMEs (these dirs have no package.json; CI runs `anchor test`). Drop the pointless `pnpm install` step and pnpm prerequisite from pure-Anchor examples that ship no JS client. - Rewrite stale [scripts] comments in create-token/nft-minter Anchor.toml that referenced removed bankrun/test.ts files and a wrong fixture name; describe the current LiteSVM + mpl_token_metadata.so fixture reality. - Remove vestigial [test] startup_wait and its false 'bankrun and validator tests' comment from token-extensions/basics (test is pure LiteSVM). - Delete orphan tests-rs/ under allow-block-list-token: its coverage is subsumed by the working test_abl_token.rs and its documented dep conflict no longer applies (repo is on solana 3.1.8). - Drop the stale 'Used in Professional Education' link from favorites. --- basics/account-data/anchor/README.md | 5 +- basics/checking-accounts/anchor/README.md | 5 +- basics/close-account/anchor/README.md | 3 +- basics/counter/anchor/README.md | 3 +- basics/create-account/anchor/README.md | 5 +- .../cross-program-invocation/anchor/README.md | 5 +- basics/favorites/anchor/README.md | 5 +- basics/hello-solana/anchor/README.md | 5 +- basics/pda-rent-payer/anchor/README.md | 5 +- .../processing-instructions/anchor/README.md | 5 +- .../anchor/README.md | 5 +- basics/pyth/anchor/README.md | 5 +- basics/realloc/anchor/README.md | 5 +- basics/rent/anchor/README.md | 5 +- basics/repository-layout/anchor/README.md | 5 +- basics/transfer-sol/anchor/README.md | 5 +- finance/token-swap/anchor/README.md | 5 +- tokens/create-token/anchor/Anchor.toml | 6 +- tokens/create-token/anchor/README.md | 5 +- tokens/nft-minter/anchor/Anchor.toml | 6 +- tokens/nft-minter/anchor/README.md | 5 +- tokens/pda-mint-authority/anchor/README.md | 5 +- .../basics/anchor/Anchor.toml | 6 - .../token-extensions/basics/anchor/README.md | 5 +- .../cpi-guard/anchor/README.md | 5 +- .../default-account-state/anchor/README.md | 5 +- .../token-extensions/group/anchor/README.md | 5 +- .../immutable-owner/anchor/README.md | 5 +- .../interest-bearing/anchor/README.md | 5 +- .../memo-transfer/anchor/README.md | 5 +- .../metadata/anchor/README.md | 5 +- .../mint-close-authority/anchor/README.md | 5 +- .../anchor-example/README.md | 2 +- .../anchor-example/anchor/README.md | 2 +- .../non-transferable/anchor/README.md | 5 +- .../permanent-delegate/anchor/README.md | 5 +- .../transfer-fee/anchor/README.md | 5 +- .../allow-block-list-token/README.md | 2 +- .../allow-block-list-token/anchor/README.md | 5 +- .../anchor/tests-rs/README.md | 26 ---- .../anchor/tests-rs/test.rs | 120 ------------------ .../transfer-hook/counter/anchor/README.md | 5 +- .../hello-world/anchor/README.md | 5 +- .../transfer-cost/anchor/README.md | 5 +- .../transfer-switch/anchor/README.md | 5 +- tokens/token-minter/anchor/README.md | 5 +- tokens/transfer-tokens/anchor/README.md | 5 +- 47 files changed, 85 insertions(+), 276 deletions(-) delete mode 100644 tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/tests-rs/README.md delete mode 100644 tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/tests-rs/test.rs diff --git a/basics/account-data/anchor/README.md b/basics/account-data/anchor/README.md index c158146a..1f6f31ce 100644 --- a/basics/account-data/anchor/README.md +++ b/basics/account-data/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../README.md). From this directory (`basics/account-data/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/checking-accounts/anchor/README.md b/basics/checking-accounts/anchor/README.md index d06c1c97..ab5c6c20 100644 --- a/basics/checking-accounts/anchor/README.md +++ b/basics/checking-accounts/anchor/README.md @@ -14,18 +14,17 @@ See also: [Checking Accounts overview](../README.md) and the [repository catalog From this directory (`basics/checking-accounts/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/close-account/anchor/README.md b/basics/close-account/anchor/README.md index b1d7c770..b3f93253 100644 --- a/basics/close-account/anchor/README.md +++ b/basics/close-account/anchor/README.md @@ -34,7 +34,6 @@ Two [instruction handlers](https://solana.com/docs/terminology#instruction-handl ## Setup ```bash -pnpm install anchor build ``` @@ -43,5 +42,5 @@ anchor build Tests live in [`programs/close-account/tests/test_close_account.rs`](programs/close-account/tests/test_close_account.rs) and run in-process with LiteSVM: ```bash -pnpm test +anchor test ``` diff --git a/basics/counter/anchor/README.md b/basics/counter/anchor/README.md index 778ed7d9..b4afefe5 100644 --- a/basics/counter/anchor/README.md +++ b/basics/counter/anchor/README.md @@ -15,14 +15,13 @@ See also: the [repository catalog](../../README.md). From `basics/counter/anchor/`: ```bash -pnpm install anchor build ``` ## Testing ```bash -pnpm test +anchor test ``` LiteSVM integration tests in `programs/counter_anchor/tests/` call handlers and assert the stored count. diff --git a/basics/create-account/anchor/README.md b/basics/create-account/anchor/README.md index 283bbe78..7f58c49f 100644 --- a/basics/create-account/anchor/README.md +++ b/basics/create-account/anchor/README.md @@ -14,18 +14,17 @@ See also: [Create Account overview](../README.md) and the [repository catalog](. From this directory (`basics/create-account/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/cross-program-invocation/anchor/README.md b/basics/cross-program-invocation/anchor/README.md index 72420c65..4d2f41a6 100644 --- a/basics/cross-program-invocation/anchor/README.md +++ b/basics/cross-program-invocation/anchor/README.md @@ -14,18 +14,17 @@ See also: [Cross Program Invocation overview](../README.md) and the [repository From this directory (`basics/cross-program-invocation/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/favorites/anchor/README.md b/basics/favorites/anchor/README.md index fb2cac1c..1194598d 100644 --- a/basics/favorites/anchor/README.md +++ b/basics/favorites/anchor/README.md @@ -12,18 +12,17 @@ See also: the [repository catalog](../../README.md). ## Setup ```bash -pnpm install anchor build ``` ## Testing ```bash -pnpm test +anchor test ``` LiteSVM tests in `programs/` assert that users cannot overwrite each other's state. ## Usage -`anchor deploy` targets the cluster in `Anchor.toml`. Used in [Solana Professional Education](https://github.com/solana-developers/professional-education). \ No newline at end of file +`anchor deploy` targets the cluster in `Anchor.toml`. \ No newline at end of file diff --git a/basics/hello-solana/anchor/README.md b/basics/hello-solana/anchor/README.md index 3c77bc30..4c8f4b72 100644 --- a/basics/hello-solana/anchor/README.md +++ b/basics/hello-solana/anchor/README.md @@ -14,18 +14,17 @@ See also: [Hello Solana overview](../README.md) and the [repository catalog](../ From this directory (`basics/hello-solana/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/pda-rent-payer/anchor/README.md b/basics/pda-rent-payer/anchor/README.md index 3a13c3ca..93c9c03d 100644 --- a/basics/pda-rent-payer/anchor/README.md +++ b/basics/pda-rent-payer/anchor/README.md @@ -14,18 +14,17 @@ See also: [Pda Rent Payer overview](../README.md) and the [repository catalog](. From this directory (`basics/pda-rent-payer/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/processing-instructions/anchor/README.md b/basics/processing-instructions/anchor/README.md index f031ec0d..6f78840e 100644 --- a/basics/processing-instructions/anchor/README.md +++ b/basics/processing-instructions/anchor/README.md @@ -14,18 +14,17 @@ See also: [Processing Instructions overview](../README.md) and the [repository c From this directory (`basics/processing-instructions/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/program-derived-addresses/anchor/README.md b/basics/program-derived-addresses/anchor/README.md index 01d33b07..70562dae 100644 --- a/basics/program-derived-addresses/anchor/README.md +++ b/basics/program-derived-addresses/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../README.md). From this directory (`basics/program-derived-addresses/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/pyth/anchor/README.md b/basics/pyth/anchor/README.md index f5cc8650..2b579dee 100644 --- a/basics/pyth/anchor/README.md +++ b/basics/pyth/anchor/README.md @@ -29,18 +29,17 @@ See also: [Pyth overview](../README.md) and the [repository catalog](../../../RE From this directory (`basics/pyth/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/realloc/anchor/README.md b/basics/realloc/anchor/README.md index 613be789..749b6283 100644 --- a/basics/realloc/anchor/README.md +++ b/basics/realloc/anchor/README.md @@ -14,18 +14,17 @@ See also: [Realloc overview](../README.md) and the [repository catalog](../../.. From this directory (`basics/realloc/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/rent/anchor/README.md b/basics/rent/anchor/README.md index 5215a52a..a9d15c64 100644 --- a/basics/rent/anchor/README.md +++ b/basics/rent/anchor/README.md @@ -14,18 +14,17 @@ See also: [Rent overview](../README.md) and the [repository catalog](../../../RE From this directory (`basics/rent/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/repository-layout/anchor/README.md b/basics/repository-layout/anchor/README.md index 372aa3cd..e452cec4 100644 --- a/basics/repository-layout/anchor/README.md +++ b/basics/repository-layout/anchor/README.md @@ -14,18 +14,17 @@ See also: [Repository Layout overview](../README.md) and the [repository catalog From this directory (`basics/repository-layout/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/basics/transfer-sol/anchor/README.md b/basics/transfer-sol/anchor/README.md index 92a37c3c..b8c0ffdd 100644 --- a/basics/transfer-sol/anchor/README.md +++ b/basics/transfer-sol/anchor/README.md @@ -14,18 +14,17 @@ See also: [Transfer Sol overview](../README.md) and the [repository catalog](../ From this directory (`basics/transfer-sol/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/finance/token-swap/anchor/README.md b/finance/token-swap/anchor/README.md index 11b48c16..1be6d893 100644 --- a/finance/token-swap/anchor/README.md +++ b/finance/token-swap/anchor/README.md @@ -15,18 +15,17 @@ See also: [Token Swap overview](../README.md) and the [repository catalog](../.. From this directory (`finance/token-swap/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/create-token/anchor/Anchor.toml b/tokens/create-token/anchor/Anchor.toml index 1b6d1e6c..0b321b29 100644 --- a/tokens/create-token/anchor/Anchor.toml +++ b/tokens/create-token/anchor/Anchor.toml @@ -13,7 +13,7 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] -# Only run bankrun tests - the validator tests (test.ts) need Metaplex Token -# Metadata cloned from mainnet which is too slow/unreliable in CI. -# bankrun.test.ts uses a local fixture (tests/fixtures/token_metadata.so). +# The LiteSVM test loads Metaplex Token Metadata from a local fixture +# (tests/fixtures/mpl_token_metadata.so) instead of cloning it from mainnet, +# which is too slow and unreliable in CI. test = "cargo test" diff --git a/tokens/create-token/anchor/README.md b/tokens/create-token/anchor/README.md index f63c34d8..e7319a04 100644 --- a/tokens/create-token/anchor/README.md +++ b/tokens/create-token/anchor/README.md @@ -14,18 +14,17 @@ See also: [Create Token overview](../README.md) and the [repository catalog](../ From this directory (`tokens/create-token/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/nft-minter/anchor/Anchor.toml b/tokens/nft-minter/anchor/Anchor.toml index 4a27cd42..f4c37a26 100644 --- a/tokens/nft-minter/anchor/Anchor.toml +++ b/tokens/nft-minter/anchor/Anchor.toml @@ -14,7 +14,7 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] -# Only run bankrun tests - the validator tests (test.ts) need Metaplex Token -# Metadata cloned from mainnet which is too slow/unreliable in CI. -# bankrun.test.ts uses a local fixture (tests/fixtures/token_metadata.so). +# The LiteSVM test loads Metaplex Token Metadata from a local fixture +# (tests/fixtures/mpl_token_metadata.so) instead of cloning it from mainnet, +# which is too slow and unreliable in CI. test = "cargo test" diff --git a/tokens/nft-minter/anchor/README.md b/tokens/nft-minter/anchor/README.md index 26071e21..e90be4d9 100644 --- a/tokens/nft-minter/anchor/README.md +++ b/tokens/nft-minter/anchor/README.md @@ -14,18 +14,17 @@ See also: [Nft Minter overview](../README.md) and the [repository catalog](../.. From this directory (`tokens/nft-minter/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/pda-mint-authority/anchor/README.md b/tokens/pda-mint-authority/anchor/README.md index a088d909..92db43f3 100644 --- a/tokens/pda-mint-authority/anchor/README.md +++ b/tokens/pda-mint-authority/anchor/README.md @@ -15,18 +15,17 @@ See also: [Pda Mint Authority overview](../README.md) and the [repository catalo From this directory (`tokens/pda-mint-authority/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/basics/anchor/Anchor.toml b/tokens/token-extensions/basics/anchor/Anchor.toml index 49b424c0..5fe29857 100644 --- a/tokens/token-extensions/basics/anchor/Anchor.toml +++ b/tokens/token-extensions/basics/anchor/Anchor.toml @@ -11,11 +11,5 @@ anchor = "6qNqxkRF791FXFeQwqYQLEzAbGiqDULC5SSHVsfRoG89" cluster = "Localnet" wallet = "~/.config/solana/id.json" -# This project has both bankrun and validator tests. No [test.validator] -# section needed because Token-2022 is built into the local validator -# and doesn't require cloning from mainnet. -[test] -startup_wait = 25000 - [scripts] test = "cargo test" diff --git a/tokens/token-extensions/basics/anchor/README.md b/tokens/token-extensions/basics/anchor/README.md index f1997633..6f4da49a 100644 --- a/tokens/token-extensions/basics/anchor/README.md +++ b/tokens/token-extensions/basics/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/basics/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/cpi-guard/anchor/README.md b/tokens/token-extensions/cpi-guard/anchor/README.md index e0220463..81c91032 100644 --- a/tokens/token-extensions/cpi-guard/anchor/README.md +++ b/tokens/token-extensions/cpi-guard/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/cpi-guard/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/default-account-state/anchor/README.md b/tokens/token-extensions/default-account-state/anchor/README.md index 1b6702ac..f219a9c5 100644 --- a/tokens/token-extensions/default-account-state/anchor/README.md +++ b/tokens/token-extensions/default-account-state/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/default-account-state/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/group/anchor/README.md b/tokens/token-extensions/group/anchor/README.md index 169b7372..8522b9b6 100644 --- a/tokens/token-extensions/group/anchor/README.md +++ b/tokens/token-extensions/group/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/group/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/immutable-owner/anchor/README.md b/tokens/token-extensions/immutable-owner/anchor/README.md index 83ee103c..3ce9be02 100644 --- a/tokens/token-extensions/immutable-owner/anchor/README.md +++ b/tokens/token-extensions/immutable-owner/anchor/README.md @@ -13,18 +13,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/immutable-owner/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/interest-bearing/anchor/README.md b/tokens/token-extensions/interest-bearing/anchor/README.md index 74b6ad19..5e09f728 100644 --- a/tokens/token-extensions/interest-bearing/anchor/README.md +++ b/tokens/token-extensions/interest-bearing/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/interest-bearing/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/memo-transfer/anchor/README.md b/tokens/token-extensions/memo-transfer/anchor/README.md index 613bc8dd..aed18d97 100644 --- a/tokens/token-extensions/memo-transfer/anchor/README.md +++ b/tokens/token-extensions/memo-transfer/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/memo-transfer/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/metadata/anchor/README.md b/tokens/token-extensions/metadata/anchor/README.md index eb758e65..68fb5d8d 100644 --- a/tokens/token-extensions/metadata/anchor/README.md +++ b/tokens/token-extensions/metadata/anchor/README.md @@ -13,18 +13,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/metadata/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/mint-close-authority/anchor/README.md b/tokens/token-extensions/mint-close-authority/anchor/README.md index 22e53fe5..4b581e6f 100644 --- a/tokens/token-extensions/mint-close-authority/anchor/README.md +++ b/tokens/token-extensions/mint-close-authority/anchor/README.md @@ -13,18 +13,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/mint-close-authority/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/README.md b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/README.md index 43d72a1f..c95cbd30 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/README.md +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/README.md @@ -15,7 +15,7 @@ A [video walkthrough](https://www.youtube.com/@SolanaFndn/videos) is available o ```bash cd anchor anchor build -pnpm test +anchor test ``` ### JS client diff --git a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/README.md b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/README.md index 6a2d9aac..9f15c62b 100644 --- a/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/README.md +++ b/tokens/token-extensions/nft-meta-data-pointer/anchor-example/anchor/README.md @@ -11,5 +11,5 @@ Copy the **[program](https://solana.com/docs/terminology#program) ID** from the anchor build anchor deploy pnpm install -pnpm test +anchor test ``` diff --git a/tokens/token-extensions/non-transferable/anchor/README.md b/tokens/token-extensions/non-transferable/anchor/README.md index ff52fbbb..b849e708 100644 --- a/tokens/token-extensions/non-transferable/anchor/README.md +++ b/tokens/token-extensions/non-transferable/anchor/README.md @@ -13,18 +13,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/non-transferable/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/permanent-delegate/anchor/README.md b/tokens/token-extensions/permanent-delegate/anchor/README.md index 7f80c076..61ef190b 100644 --- a/tokens/token-extensions/permanent-delegate/anchor/README.md +++ b/tokens/token-extensions/permanent-delegate/anchor/README.md @@ -13,18 +13,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/permanent-delegate/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/transfer-fee/anchor/README.md b/tokens/token-extensions/transfer-fee/anchor/README.md index 1e4b0b78..4e5c38da 100644 --- a/tokens/token-extensions/transfer-fee/anchor/README.md +++ b/tokens/token-extensions/transfer-fee/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../README.md). From this directory (`tokens/token-extensions/transfer-fee/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/README.md b/tokens/token-extensions/transfer-hook/allow-block-list-token/README.md index ac83f8aa..5a469778 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/README.md +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/README.md @@ -38,7 +38,7 @@ pnpm run dev # serve the UI From `anchor/`: ```bash -pnpm test +anchor test ``` LiteSVM integration tests run via `cargo test` (see `Anchor.toml`). The UI is optional and targets devnet or your own cluster configuration. diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/README.md b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/README.md index 9504a5d1..53978479 100644 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/README.md +++ b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/README.md @@ -15,18 +15,17 @@ See also: [Allow Block List Token overview](../README.md) and the [repository ca From this directory (`tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/tests-rs/README.md b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/tests-rs/README.md deleted file mode 100644 index 4305f3c1..00000000 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/tests-rs/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Temporarily relocated Rust tests - -`test.rs` was moved here from `programs/abl-token/tests/test.rs` because it -cannot currently compile alongside anchor-lang 0.32.1. - -## Why - -The test uses **litesvm**, which pins `solana-account-info` to an exact 2.2.x -version. However, anchor-lang 0.32.1 requires `solana-account-info >=2.3.0` -for the `AccountInfo::resize()` method (renamed from `realloc` in 2.3.0). - -This creates an unresolvable dependency conflict: - -- **litesvm <=0.6** → pins `solana-account-info =2.2.1` (too old, no `resize()`) -- **litesvm 0.9+** → uses `solana-account-info 3.x` (type mismatch with anchor-lang's 2.x types) - -No litesvm release currently targets the `solana-account-info 2.3+` range -that anchor-lang 0.32.1 needs. - -## When to move it back - -Move `test.rs` back to `programs/abl-token/tests/test.rs` and restore the -`[dev-dependencies]` in `Cargo.toml` when **either**: - -1. **anchor-lang upgrades to solana 3.x** (likely 0.33+), so litesvm 0.9+ types match, or -2. **litesvm releases a version** targeting `solana-account-info >=2.3, <3` diff --git a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/tests-rs/test.rs b/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/tests-rs/test.rs deleted file mode 100644 index 0f9ecc47..00000000 --- a/tokens/token-extensions/transfer-hook/allow-block-list-token/anchor/tests-rs/test.rs +++ /dev/null @@ -1,120 +0,0 @@ -// TODO: Move this test back to programs/abl-token/tests/test.rs once the litesvm -// version gap is resolved. Currently litesvm <=0.6 pins solana-account-info to -// =2.2.1, but anchor-lang 0.32.1 needs >=2.3.0 (for AccountInfo::resize). -// litesvm 0.9+ uses solana 3.x which has type mismatches with anchor-lang's v2 types. -// This will resolve when anchor-lang upgrades to solana 3.x (likely anchor 0.33+). - -use { - abl_token::{accounts::InitConfigAccountConstraints, accounts::InitMintAccountConstraints, instructions::InitMintArgs, Mode}, - anchor_lang::InstructionData, - anchor_lang::ToAccountMetas, - litesvm::LiteSVM, - solana_instruction::Instruction, - solana_keypair::Keypair, - solana_message::Message, - solana_native_token::LAMPORTS_PER_SOL, - solana_pubkey::{pubkey, Pubkey}, - solana_sdk_ids::system_program::ID as SYSTEM_PROGRAM_ID, - solana_signer::Signer, - solana_transaction::Transaction, - spl_token_2022::ID as TOKEN_22_PROGRAM_ID, - std::path::PathBuf, -}; - -const PROGRAM_ID: Pubkey = abl_token::ID_CONST; - -fn setup() -> (LiteSVM, Keypair) { - let mut svm = LiteSVM::new(); - let admin_kp = Keypair::new(); - let admin_pk = admin_kp.pubkey(); - - svm.airdrop(&admin_pk, 10000 * LAMPORTS_PER_SOL).unwrap(); - - let mut so_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - so_path.push("../../target/deploy/abl_token.so"); - - println!("Deploying program from {}", so_path.display()); - - let bytecode = std::fs::read(so_path).unwrap(); - - svm.add_program(PROGRAM_ID, &bytecode); - - (svm, admin_kp) -} - -#[test] -fn test() { - let (mut svm, admin_kp) = setup(); - let admin_pk = admin_kp.pubkey(); - - let mint_kp = Keypair::new(); - let mint_pk = mint_kp.pubkey(); - let config = derive_config(); - let meta_list = derive_meta_list(&mint_pk); - - let init_cfg_ix = abl_token::instruction::InitConfig {}; - - let init_cfg_accounts = InitConfigAccountConstraints { - payer: admin_pk, - config: config, - system_program: SYSTEM_PROGRAM_ID, - }; - - let accs = init_cfg_accounts.to_account_metas(None); - - let instruction = Instruction { - program_id: PROGRAM_ID, - accounts: accs, - data: init_cfg_ix.data(), - }; - let msg = Message::new(&[instruction], Some(&admin_pk)); - let tx = Transaction::new(&[&admin_kp], msg, svm.latest_blockhash()); - - svm.send_transaction(tx).unwrap(); - - let args: InitMintArgs = InitMintArgs { - name: "Test".to_string(), - symbol: "TEST".to_string(), - uri: "https://test.com".to_string(), - decimals: 6, - mint_authority: mint_pk, - freeze_authority: mint_pk, - permanent_delegate: mint_pk, - transfer_hook_authority: admin_pk, - mode: Mode::Mixed, - threshold: 100000, - }; - let init_mint_ix = abl_token::instruction::InitMint { args: args }; - - let data = init_mint_ix.data(); - - let init_mint_accounts = InitMintAccountConstraints { - payer: admin_pk, - mint: mint_pk, - extra_metas_account: meta_list, - system_program: SYSTEM_PROGRAM_ID, - token_program: TOKEN_22_PROGRAM_ID, - }; - - let accs = init_mint_accounts.to_account_metas(None); - - let instruction = Instruction { - program_id: PROGRAM_ID, - accounts: accs, - data: data, - }; - let msg = Message::new(&[instruction], Some(&admin_pk)); - let tx = Transaction::new(&[&admin_kp, &mint_kp], msg, svm.latest_blockhash()); - - let _res = svm.send_transaction(tx).unwrap(); -} - -fn derive_config() -> Pubkey { - let seeds = &[b"config".as_ref()]; - Pubkey::find_program_address(seeds, &PROGRAM_ID).0 -} - -fn derive_meta_list(mint: &Pubkey) -> Pubkey { - let seeds = &[b"extra-account-metas", mint.as_ref()]; - Pubkey::find_program_address(seeds, &PROGRAM_ID).0 -} diff --git a/tokens/token-extensions/transfer-hook/counter/anchor/README.md b/tokens/token-extensions/transfer-hook/counter/anchor/README.md index 39c0c36c..9598489e 100644 --- a/tokens/token-extensions/transfer-hook/counter/anchor/README.md +++ b/tokens/token-extensions/transfer-hook/counter/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../../README.md). From this directory (`tokens/token-extensions/transfer-hook/counter/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/transfer-hook/hello-world/anchor/README.md b/tokens/token-extensions/transfer-hook/hello-world/anchor/README.md index 32abbd60..5a6cbb8a 100644 --- a/tokens/token-extensions/transfer-hook/hello-world/anchor/README.md +++ b/tokens/token-extensions/transfer-hook/hello-world/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../../README.md). From this directory (`tokens/token-extensions/transfer-hook/hello-world/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/README.md b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/README.md index 76b2157c..8c082fd0 100644 --- a/tokens/token-extensions/transfer-hook/transfer-cost/anchor/README.md +++ b/tokens/token-extensions/transfer-hook/transfer-cost/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../../README.md). From this directory (`tokens/token-extensions/transfer-hook/transfer-cost/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/README.md b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/README.md index 28fe75c0..036eeb34 100644 --- a/tokens/token-extensions/transfer-hook/transfer-switch/anchor/README.md +++ b/tokens/token-extensions/transfer-hook/transfer-switch/anchor/README.md @@ -14,18 +14,17 @@ See also: the [repository catalog](../../../../../README.md). From this directory (`tokens/token-extensions/transfer-hook/transfer-switch/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/token-minter/anchor/README.md b/tokens/token-minter/anchor/README.md index d6719642..b0473a15 100644 --- a/tokens/token-minter/anchor/README.md +++ b/tokens/token-minter/anchor/README.md @@ -15,18 +15,17 @@ See also: [Token Minter overview](../README.md) and the [repository catalog](../ From this directory (`tokens/token-minter/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. diff --git a/tokens/transfer-tokens/anchor/README.md b/tokens/transfer-tokens/anchor/README.md index b173325e..317a537f 100644 --- a/tokens/transfer-tokens/anchor/README.md +++ b/tokens/transfer-tokens/anchor/README.md @@ -16,18 +16,17 @@ See also: [Transfer Tokens overview](../README.md) and the [repository catalog]( From this directory (`tokens/transfer-tokens/anchor/`): ```bash -pnpm install anchor build ``` -Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs), and `pnpm`. +Prerequisites: [Agave](https://docs.anza.xyz/) CLI (version in `Anchor.toml` `[toolchain]`), [Anchor](https://www.anchor-lang.com/docs). ## Testing Tests run in-process with [LiteSVM](https://www.anchor-lang.com/docs/testing/litesvm). No local validator. ```bash -pnpm test +anchor test ``` This runs `cargo test` as configured in `Anchor.toml`. Tests call instruction handlers and check onchain state. From 5b6294b8227664cbd3562f91f4884e5ebb392a6b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 14:26:04 +0000 Subject: [PATCH 2/5] docs(finance): convert tables to prose, remove em-dashes, fix terminology Tables in finance READMEs render badly in plain text, so convert them to prose bullets (`**key**: value` for concept/cast rows, backticked-key colon form for code identifiers). Remove all em-dashes in favour of commas, colons, or parentheses. Content, numbers, identifiers, math symbols, and links are preserved verbatim. Also fixes real terminology issues surfaced while editing: - perpetual-futures/anchor: 'on-chain' -> 'onchain' - token-swap: 'minus gas' -> 'minus transaction fees' - betting-market: reword the overclaimed 'trustless betting' (custody is bypass-proof, but outcome resolution is admin-trusted). Covers 12 finance READMEs; order-book/anchor and the two lending READMEs follow in the next commit. --- finance/betting-market/anchor/README.md | 5 +- finance/betting-market/kani-proofs/README.md | 26 ++-- finance/escrow/kani-proofs/README.md | 32 ++--- finance/lending/kani-proofs/README.md | 42 +++--- finance/order-book/kani-proofs/README.md | 34 ++--- finance/perpetual-futures/anchor/README.md | 130 ++++++++---------- finance/perpetual-futures/quasar/README.md | 2 +- .../token-fundraiser/kani-proofs/README.md | 10 +- finance/token-swap/README.md | 2 +- finance/token-swap/kani-proofs/README.md | 46 +++---- finance/vault-strategy/anchor/README.md | 6 +- finance/vault-strategy/kani-proofs/README.md | 20 ++- 12 files changed, 158 insertions(+), 197 deletions(-) diff --git a/finance/betting-market/anchor/README.md b/finance/betting-market/anchor/README.md index 9337779b..f914d282 100644 --- a/finance/betting-market/anchor/README.md +++ b/finance/betting-market/anchor/README.md @@ -10,8 +10,9 @@ where odds are set by the crowd's stakes rather than by an order book or a fixed ## Purpose -It solves the core problem of trustless betting: collecting stakes from many bettors, holding them -in one place no single bettor controls, and paying winners by a fixed, public formula. The pool is +It solves the core custody problem of pooled betting: collecting stakes from many bettors, holding +them in one place no single bettor controls, and paying winners by a fixed, public formula. (Outcome +resolution is still trusted: the admin chooses the winning outcome, as described below.) The pool is a token account owned by the event's PDA, so payouts are signed by the program with the event's seeds - there is no admin key that can move bettors' stakes out of the pool. The admin's only powers are creating events/outcomes and choosing the winning outcome (or cancelling). diff --git a/finance/betting-market/kani-proofs/README.md b/finance/betting-market/kani-proofs/README.md index fc5c8914..457ec83c 100644 --- a/finance/betting-market/kani-proofs/README.md +++ b/finance/betting-market/kani-proofs/README.md @@ -1,4 +1,4 @@ -# Betting-market — Kani proofs +# Betting-market: Kani proofs Formal-verification harnesses for the pari-mutuel betting market, in the spirit of [`aeyakovenko/percolator`](https://github.com/aeyakovenko/percolator), which @@ -12,12 +12,10 @@ split among the winners in proportion to their stake. The token movement goes through SPL CPIs Kani cannot symbolically execute, but the payout math is pure integer arithmetic. This crate reproduces it faithfully and proves: -| Harness | Property | -| --- | --- | -| `proof_settlement_fee_and_split` | `fee <= losing_pool` (so `distributable` never underflows) and `winning + distributable + fee == total` — settlement conserves the pool. | -| `proof_winner_never_below_stake` | `payout = stake + winnings >= stake`: a winner is never paid less than they staked (the fee is charged only on losers). | -| `proof_parimutuel_solvency` | **Solvency** (centrepiece): the winners collectively never claim more than the vault holds after the fee (`Σ payout_i <= winning_pool + distributable`). | -| `proof_refund_conserves_pool` | On cancellation, refunds sum back to the total pool — neither over- nor under-drained. | +- `proof_settlement_fee_and_split`: `fee <= losing_pool` (so `distributable` never underflows) and `winning + distributable + fee == total`, settlement conserves the pool. +- `proof_winner_never_below_stake`: `payout = stake + winnings >= stake`: a winner is never paid less than they staked (the fee is charged only on losers). +- `proof_parimutuel_solvency`: **Solvency** (centrepiece): the winners collectively never claim more than the vault holds after the fee (`Σ payout_i <= winning_pool + distributable`). +- `proof_refund_conserves_pool`: On cancellation, refunds sum back to the total pool, neither over- nor under-drained. ### The solvency proof @@ -25,7 +23,7 @@ After settlement the vault holds `winning_pool + distributable_losing_pool`. Each winner is paid `stake_i + floor(stake_i · D / winning_pool)`, and the winning stakes sum to `winning_pool`. Because `Σ floor(stake_i·D/W) <= Σ stake_i·D/W = D`, total payouts are -`<= winning_pool + D` — exactly the vault balance. So no set of winners can drain +`<= winning_pool + D`, exactly the vault balance. So no set of winners can drain the vault below zero; floor rounding only ever leaves dust behind. Modelled with 3 winners whose stakes sum to the winning pool. @@ -33,17 +31,15 @@ the vault below zero; floor rounding only ever leaves dust behind. Modelled with The settlement, payout, and solvency proofs verify nonlinear 128-bit arithmetic (`stake · distributable`, divided by the symbolic winning pool), the hard case -for a bit-precise solver, so — as percolator does — they bound their symbolic +for a bit-precise solver, so (as percolator does) they bound their symbolic inputs to a representative range; the pro-rata identity is scale-invariant. The refund proof is pure linear logic and runs at full `u64` width (bounded only in the number of bettors). -| Harness | Bound | Time | -| --- | --- | --- | -| `proof_settlement_fee_and_split` | `total_pool <= 4095`, `fee_bps` symbolic | ~1s | -| `proof_winner_never_below_stake` | `winning_pool/distributable <= 255` | ~6s | -| `proof_parimutuel_solvency` | 3 winners, stakes `<= 7`, `distributable <= 63` | ~3s | -| `proof_refund_conserves_pool` | 4 bettors, full `u64` | <1s | +- `proof_settlement_fee_and_split`: `total_pool <= 4095`, `fee_bps` symbolic, ~1s +- `proof_winner_never_below_stake`: `winning_pool/distributable <= 255`, ~6s +- `proof_parimutuel_solvency`: 3 winners, stakes `<= 7`, `distributable <= 63`, ~3s +- `proof_refund_conserves_pool`: 4 bettors, full `u64`, <1s Run weekly in CI (the `.github/workflows/kani.yml` `verify` job), not on every push/PR, because the bounded nonlinear proofs are slow. A fast unit-test job runs per push/PR. diff --git a/finance/escrow/kani-proofs/README.md b/finance/escrow/kani-proofs/README.md index b9f60bc8..40b7e63f 100644 --- a/finance/escrow/kani-proofs/README.md +++ b/finance/escrow/kani-proofs/README.md @@ -1,4 +1,4 @@ -# Escrow — Kani proofs +# Escrow: Kani proofs Formal-verification harnesses for the escrow program, in the spirit of [`aeyakovenko/percolator`](https://github.com/aeyakovenko/percolator), which @@ -7,24 +7,22 @@ the mathematical correctness of a DeFi engine. ## What is verified -The escrow program itself does almost no arithmetic — it delegates token +The escrow program itself does almost no arithmetic, it delegates token movement to the SPL token program through CPIs, which Kani cannot symbolically execute. So (exactly like percolator, which verifies a self-contained library) this crate models the escrow's *verifiable core* as pure Rust functions that mirror the on-chain code's arithmetic and statement ordering, and proves the invariants the program relies on: -| Harness | Property | -| --- | --- | -| `proof_token_transfer_conserves` | An SPL transfer either fails atomically or conserves the two accounts' total balance. | -| `proof_close_offer_conserves_on_success` | Closing the offer account conserves lamports and empties the source. | -| `proof_close_offer_conserves_lamports_unconditionally` | **Finding (now fixed)**: lamport conservation holds with equality on every path (see below). | -| `proof_take_offer_conserves_value` | A take conserves total mint A and total mint B, drains the vault, and pays the maker exactly the price. | -| `proof_take_offer_guard_never_overflows` | The `checked_add` conservation guards in `take_offer` are unreachable dead code. | -| `proof_take_offer_guard_dead_under_spl_invariant` | Same, shown explicitly under the SPL supply invariant. | -| `proof_cancel_offer_returns_all_to_maker` | Cancelling returns every vault token to the maker and conserves mint A. | -| `proof_make_offer_vault_equals_offered` | After a deposit the vault holds exactly the offered amount. | -| `proof_offer_id_le_bytes_roundtrip` / `proof_offer_id_seed_injective` | The PDA `id` seed encoding is a lossless, injective round-trip. | +- `proof_token_transfer_conserves`: An SPL transfer either fails atomically or conserves the two accounts' total balance. +- `proof_close_offer_conserves_on_success`: Closing the offer account conserves lamports and empties the source. +- `proof_close_offer_conserves_lamports_unconditionally`: **Finding (now fixed)**: lamport conservation holds with equality on every path (see below). +- `proof_take_offer_conserves_value`: A take conserves total mint A and total mint B, drains the vault, and pays the maker exactly the price. +- `proof_take_offer_guard_never_overflows`: The `checked_add` conservation guards in `take_offer` are unreachable dead code. +- `proof_take_offer_guard_dead_under_spl_invariant`: Same, shown explicitly under the SPL supply invariant. +- `proof_cancel_offer_returns_all_to_maker`: Cancelling returns every vault token to the maker and conserves mint A. +- `proof_make_offer_vault_equals_offered`: After a deposit the vault holds exactly the offered amount. +- `proof_offer_id_le_bytes_roundtrip` / `proof_offer_id_seed_injective`: The PDA `id` seed encoding is a lossless, injective round-trip. ## Finding (now fixed): lamport ordering in `close_offer_account` @@ -40,11 +38,11 @@ invariants the program relies on: At `offer == dest == u64::MAX` the credit overflows and returns `Err` after the source was already zeroed, so the total *transiently* dropped from `2·MAX` to -`MAX` — lamports momentarily destroyed on the error path. Not exploitable (the +`MAX`: lamports momentarily destroyed on the error path. Not exploitable (the runtime reverts on `Err`, and a wallet can't hold near `u64::MAX` lamports), but conservation held only because of those *external* guarantees. -**The fix (applied):** `close_offer_account` now uses *compute-then-commit* — +**The fix (applied):** `close_offer_account` now uses *compute-then-commit*: the `checked_add` runs **before** any account is mutated, so the error path changes nothing: @@ -57,7 +55,7 @@ let new_destination_lamports = destination_lamports ``` `proof_close_offer_conserves_lamports_unconditionally` now proves lamport -conservation holds with **equality on every path**, with no precondition — the +conservation holds with **equality on every path**, with no precondition, the invariant no longer depends on the runtime reverting a failed instruction. (This is also why it's a plain proof, not a `#[kani::should_panic]`: a should-panic encoding would have *started failing* the moment this fix landed.) @@ -65,7 +63,7 @@ encoding would have *started failing* the moment this fix landed.) ## CI These proofs run **weekly** (and on demand) in the `.github/workflows/kani.yml` -`verify` job, alongside the other `finance/` proof crates — the nonlinear ones +`verify` job, alongside the other `finance/` proof crates, the nonlinear ones are slow, so the full Kani run is scheduled rather than gating every push/PR. A fast `cargo test` job runs per push/PR to catch model regressions early. diff --git a/finance/lending/kani-proofs/README.md b/finance/lending/kani-proofs/README.md index 8244290e..6b7d23b8 100644 --- a/finance/lending/kani-proofs/README.md +++ b/finance/lending/kani-proofs/README.md @@ -1,11 +1,11 @@ -# Lending — Kani proofs +# Lending: Kani proofs Formal-verification harnesses for the lending program, in the spirit of [`aeyakovenko/percolator`](https://github.com/aeyakovenko/percolator), which uses the [Kani](https://github.com/model-checking/kani) model checker to prove the mathematical correctness of a DeFi engine. -This is the richest of the finance examples — a Solend-style pool — so it gets +This is the richest of the finance examples (a Solend-style pool) so it gets the most proofs. ## What is verified @@ -14,20 +14,18 @@ The token movement is delegated to SPL CPIs Kani cannot symbolically execute, but the money-math is pure integer arithmetic. This crate reproduces the formulas faithfully and proves their invariants: -| Harness | Property | -| --- | --- | -| `proof_mul_div_floor_ceil_correct` | `mul_div_floor`/`mul_div_ceil` are the true floor/ceil of `a·b/d`, differ by ≤ 1, and coincide iff the division is exact. | -| `proof_rounding_is_protocol_favourable` | `ceil ≥ floor` always — debt (rounded up) is never undercounted and a supplier claim (rounded down) never overcounted, so dust can't be extracted by round-trips. | -| `proof_interest_index_monotonic` | The cumulative borrow-rate index never decreases (`accrue_interest` multiplies by a factor ≥ 1) — borrowers always owe ≥ principal. | -| `proof_utilization_in_range` | Utilization is always a valid `[0, 10000]` bps fraction (`borrowed ≤ gross`). | -| `proof_borrow_rate_within_bounds` | The kinked rate curve stays within `[min_rate, max_rate]` for every utilization, given the config ordering `min ≤ optimal ≤ max`. | -| `proof_deposit_redeem_cannot_extract` | A deposit→redeem round-trip never returns more liquidity than was put in (both legs floor) — no rounding drain of the pool. | -| `proof_liquidation_repay_bounded_by_debt` | A liquidation never repays more than the debt (close factor ≤ 100% ⇒ `max_repay ≤ debt`). | -| `proof_seize_value_includes_bonus` | Seized value always includes the bonus (`seize ≥ repay_value`) — the liquidator is never under-compensated. | +- `proof_mul_div_floor_ceil_correct`: `mul_div_floor`/`mul_div_ceil` are the true floor/ceil of `a·b/d`, differ by ≤ 1, and coincide iff the division is exact. +- `proof_rounding_is_protocol_favourable`: `ceil ≥ floor` always, debt (rounded up) is never undercounted and a supplier claim (rounded down) never overcounted, so dust can't be extracted by round-trips. +- `proof_interest_index_monotonic`: The cumulative borrow-rate index never decreases (`accrue_interest` multiplies by a factor ≥ 1), borrowers always owe ≥ principal. +- `proof_utilization_in_range`: Utilization is always a valid `[0, 10000]` bps fraction (`borrowed ≤ gross`). +- `proof_borrow_rate_within_bounds`: The kinked rate curve stays within `[min_rate, max_rate]` for every utilization, given the config ordering `min ≤ optimal ≤ max`. +- `proof_deposit_redeem_cannot_extract`: A deposit→redeem round-trip never returns more liquidity than was put in (both legs floor), no rounding drain of the pool. +- `proof_liquidation_repay_bounded_by_debt`: A liquidation never repays more than the debt (close factor ≤ 100% ⇒ `max_repay ≤ debt`). +- `proof_seize_value_includes_bonus`: Seized value always includes the bonus (`seize ≥ repay_value`), the liquidator is never under-compensated. ## Bounded model checking -All these harnesses verify **nonlinear 128-bit arithmetic** — and several divide +All these harnesses verify **nonlinear 128-bit arithmetic**, and several divide by a *symbolic* divisor (`mul_div`'s `d`, the index `scale`, the rate curve's `full − optimal`), the single most expensive shape for a bit-precise solver. Following percolator's practice, each bounds its symbolic inputs to a @@ -43,16 +41,14 @@ so the proof can use a small one: (dividing by a symbolic value near 10_000 is intractable; the in-bounds property is identical at any scale). -| Harness | Bound | Time | -| --- | --- | --- | -| `proof_mul_div_floor_ceil_correct` | `a, b, d <= 31` | ~37s | -| `proof_rounding_is_protocol_favourable` | `a, b, d <= 127` | ~29s | -| `proof_interest_index_monotonic` | `old/accrued <= 255`, `scale <= 127` | ~5s | -| `proof_utilization_in_range` | `<= 4095` | ~1s | -| `proof_borrow_rate_within_bounds` | rates `<= 255`, `full_utilization <= 32` | ~25s | -| `proof_deposit_redeem_cannot_extract` | `<= 31` | ~6s | -| `proof_liquidation_repay_bounded_by_debt` | `debt <= 4095` | <1s | -| `proof_seize_value_includes_bonus` | `repay_value <= 4095` | <1s | +- `proof_mul_div_floor_ceil_correct`: `a, b, d <= 31`, ~37s +- `proof_rounding_is_protocol_favourable`: `a, b, d <= 127`, ~29s +- `proof_interest_index_monotonic`: `old/accrued <= 255`, `scale <= 127`, ~5s +- `proof_utilization_in_range`: `<= 4095`, ~1s +- `proof_borrow_rate_within_bounds`: rates `<= 255`, `full_utilization <= 32`, ~25s +- `proof_deposit_redeem_cannot_extract`: `<= 31`, ~6s +- `proof_liquidation_repay_bounded_by_debt`: `debt <= 4095`, <1s +- `proof_seize_value_includes_bonus`: `repay_value <= 4095`, <1s These proofs run **weekly in CI** (the `kani.yml` `verify` job), not on every push/PR, because they are slow. A fast unit-test job runs per push/PR. diff --git a/finance/order-book/kani-proofs/README.md b/finance/order-book/kani-proofs/README.md index 379c5673..66f0e499 100644 --- a/finance/order-book/kani-proofs/README.md +++ b/finance/order-book/kani-proofs/README.md @@ -1,4 +1,4 @@ -# Order-book — Kani proofs +# Order-book: Kani proofs Formal-verification harnesses for the order-book program, in the spirit of [`aeyakovenko/percolator`](https://github.com/aeyakovenko/percolator), which @@ -8,19 +8,17 @@ the mathematical correctness of a DeFi engine. ## What is verified The on-chain instructions move tokens through SPL CPIs that Kani cannot -symbolically execute, but the program's interesting logic — the price-time +symbolically execute, but the program's interesting logic (the price-time matching engine, the maker-funded ceiling fee, the taker's price-improvement -rebate, and the two-lot price/quantity conversions — is pure integer +rebate, and the two-lot price/quantity conversions) is pure integer arithmetic. This crate reproduces those formulas faithfully and proves their invariants: -| Harness | Property | -| --- | --- | -| `proof_matching_conserves_quantity` | **Matching conservation**: `total_filled + taker_remaining == incoming_quantity` (and so `place_order`'s `quantity.checked_sub(taker_remaining)` never underflows). | -| `proof_matching_respects_price_and_maker_size` | Every fill clears at a price that crosses the taker's limit and never exceeds the resting maker's size. | -| `proof_fee_is_ceiling_and_bounded` | `fee = ⌈gross·bps/10000⌉` is a true ceiling and never exceeds `gross` — so `gross − fee` never underflows and the `require!(fee_quote <= gross_quote)` guard is unreachable dead code. | -| `proof_bid_rebate_is_non_negative` | A taker bid locks at its limit but fills at the (better) maker price, so `locked − gross ≥ 0`: the price-improvement rebate never underflows. | -| `proof_remaining_quantity_consistent` | `remaining + filled == original`, `remaining <= original`. | +- `proof_matching_conserves_quantity`: **Matching conservation**: `total_filled + taker_remaining == incoming_quantity` (and so `place_order`'s `quantity.checked_sub(taker_remaining)` never underflows). +- `proof_matching_respects_price_and_maker_size`: Every fill clears at a price that crosses the taker's limit and never exceeds the resting maker's size. +- `proof_fee_is_ceiling_and_bounded`: `fee = ⌈gross·bps/10000⌉` is a true ceiling and never exceeds `gross`, so `gross − fee` never underflows and the `require!(fee_quote <= gross_quote)` guard is unreachable dead code. +- `proof_bid_rebate_is_non_negative`: A taker bid locks at its limit but fills at the (better) maker price, so `locked − gross ≥ 0`: the price-improvement rebate never underflows. +- `proof_remaining_quantity_consistent`: `remaining + filled == original`, `remaining <= original`. ## Bounded model checking @@ -28,17 +26,15 @@ The matching/bookkeeping proofs are pure linear logic and run at **full `u64` width** (only the book depth is bounded, to 4 resting leaves via `unwind`). The fee and rebate proofs verify **nonlinear 128-bit arithmetic** (`gross·bps`, and the three-way `price·qty·lot` product), the hard case for a bit-precise solver, -so — as percolator does — they bound their symbolic inputs to a representative +so (as percolator does) they bound their symbolic inputs to a representative range. The identities are scale-invariant, so the bounded domain still exercises every rounding / crossing boundary. -| Harness | Bound | Time | -| --- | --- | --- | -| `proof_matching_conserves_quantity` | 4 leaves, full `u64` values | ~43s | -| `proof_matching_respects_price_and_maker_size` | 4 leaves, full `u64` values | <1s | -| `proof_fee_is_ceiling_and_bounded` | `gross <= 255`, `bps` fully symbolic | ~75s | -| `proof_bid_rebate_is_non_negative` | prices/qty/lot `<= 31` | ~3s | -| `proof_remaining_quantity_consistent` | full `u64` | <1s | +- `proof_matching_conserves_quantity`: 4 leaves, full `u64` values, ~43s +- `proof_matching_respects_price_and_maker_size`: 4 leaves, full `u64` values, <1s +- `proof_fee_is_ceiling_and_bounded`: `gross <= 255`, `bps` fully symbolic, ~75s +- `proof_bid_rebate_is_non_negative`: prices/qty/lot `<= 31`, ~3s +- `proof_remaining_quantity_consistent`: full `u64`, <1s These proofs run **weekly in CI** (the `kani.yml` `verify` job), not on every push/PR, because they are slow. A fast unit-test job runs per push/PR. @@ -48,7 +44,7 @@ push/PR, because they are slow. A fast unit-test job runs per push/PR. - The ceiling fee can make `fee == gross` on dust fills (e.g. `gross = 1`), so a maker can net zero quote on a sub-unit fill. This is intended (the comment in `place_order` notes ceiling rounding is in the protocol's favour to stop - fee-dust farming), not a bug — the proof confirms `fee <= gross` always holds, + fee-dust farming), not a bug: the proof confirms `fee <= gross` always holds, so the maker is never *overdrawn*. ## Running diff --git a/finance/perpetual-futures/anchor/README.md b/finance/perpetual-futures/anchor/README.md index 25b4a3fe..709042f6 100644 --- a/finance/perpetual-futures/anchor/README.md +++ b/finance/perpetual-futures/anchor/README.md @@ -1,6 +1,6 @@ # Perpetual Futures -A perpetual futures exchange — a venue for making leveraged bets on an asset's price without ever owning the asset. It is modelled on the oracle-priced, pool-collateralized design used by [Jupiter Perpetuals](https://station.jup.ag/guides/perpetual-exchange/overview) and GMX (and the open-source [`solana-labs/perpetuals`](https://github.com/solana-labs/perpetuals) reference that [Adrena](https://github.com/AdrenaFoundation/adrena-program) and [Flash Trade](https://github.com/flash-trade/flash-perpetuals) fork), rather than the order-book design used by [Drift](https://docs.drift.trade/). +A perpetual futures exchange: a venue for making leveraged bets on an asset's price without ever owning the asset. It is modelled on the oracle-priced, pool-collateralized design used by [Jupiter Perpetuals](https://station.jup.ag/guides/perpetual-exchange/overview) and GMX (and the open-source [`solana-labs/perpetuals`](https://github.com/solana-labs/perpetuals) reference that [Adrena](https://github.com/AdrenaFoundation/adrena-program) and [Flash Trade](https://github.com/flash-trade/flash-perpetuals) fork), rather than the order-book design used by [Drift](https://docs.drift.trade/). The collateral is **USDC** (a dollar stablecoin), and the market tracks the price of **NVDAx**, a tokenised Nvidia share whose [oracle](#oracle) price follows the real stock. A second market could track **TSLAx** (Tesla); each market is one collateral token plus one price feed. In the tests these are mock [SPL tokens](https://solana.com/docs/terminology#token). @@ -12,12 +12,10 @@ A [perpetual future](https://www.investopedia.com/terms/f/futurescontract.asp) ( ## Programs -| Program | Description | -|---------|-------------| -| `perpetual-futures` | The exchange: pool creation, liquidity provision, opening/closing leveraged positions, funding, liquidation, and fee collection. | -| `mock-switchboard` | Test-only price feed. Stores a price, scale, last-update slot, and confidence band that tests write directly. Replaced by a real [Switchboard](https://docs.switchboard.xyz/) On-Demand feed in production. | +- `perpetual-futures`: The exchange: pool creation, liquidity provision, opening/closing leveraged positions, funding, liquidation, and fee collection. +- `mock-switchboard`: Test-only price feed. Stores a price, scale, last-update slot, and confidence band that tests write directly. Replaced by a real [Switchboard](https://docs.switchboard.xyz/) On-Demand feed in production. -All money math is integer `u128` with `checked_*` operations, multiplying before dividing and rounding in the pool's favour — no floats, no fixed-point library. +All money math is integer `u128` with `checked_*` operations, multiplying before dividing and rounding in the pool's favour: no floats, no fixed-point library. --- @@ -25,7 +23,7 @@ All money math is integer `u128` with `checked_*` operations, multiplying before ### Long and short, leverage, collateral -A trader goes [long](https://www.investopedia.com/terms/l/long.asp) if they think the price will rise or [short](https://www.investopedia.com/terms/s/short.asp) if they think it will fall. They post [collateral](https://www.investopedia.com/terms/c/collateral.asp) and choose a position size up to the pool's maximum [leverage](https://www.investopedia.com/terms/l/leverage.asp) (borrowing power). The [notional size](https://www.investopedia.com/terms/n/notionalvalue.asp) is the full exposure — e.g. $5,000 even if only $1,000 of collateral was posted — and profit or loss is the notional times the percentage change in price: +A trader goes [long](https://www.investopedia.com/terms/l/long.asp) if they think the price will rise or [short](https://www.investopedia.com/terms/s/short.asp) if they think it will fall. They post [collateral](https://www.investopedia.com/terms/c/collateral.asp) and choose a position size up to the pool's maximum [leverage](https://www.investopedia.com/terms/l/leverage.asp) (borrowing power). The [notional size](https://www.investopedia.com/terms/n/notionalvalue.asp) is the full exposure (e.g. $5,000 even if only $1,000 of collateral was posted) and profit or loss is the notional times the percentage change in price: ``` long profit/loss = size * (price - entry_price) / entry_price @@ -34,11 +32,11 @@ short profit/loss = size * (entry_price - price) / entry_price ### The liquidity pool and provider shares -There is no order book. Every trade is against one shared [liquidity pool](https://www.investopedia.com/terms/l/liquidity.asp) that other users fund; the pool is the counterparty to all of them — it pays trader profits and keeps trader losses. Providers receive shares priced against [mark-to-market](https://www.investopedia.com/terms/m/marktomarket.asp) assets-under-management (the pool's value if every open position were settled now), derived from running per-side accumulators rather than by iterating positions. Pricing against the marked value stops a provider exiting just before an in-flight trader profit is realized. The first deposit mints `deposit - MINIMUM_LIQUIDITY` shares (the Uniswap V2 convention) so the share supply never starts at a dust amount. +There is no order book. Every trade is against one shared [liquidity pool](https://www.investopedia.com/terms/l/liquidity.asp) that other users fund; the pool is the counterparty to all of them: it pays trader profits and keeps trader losses. Providers receive shares priced against [mark-to-market](https://www.investopedia.com/terms/m/marktomarket.asp) assets-under-management (the pool's value if every open position were settled now), derived from running per-side accumulators rather than by iterating positions. Pricing against the marked value stops a provider exiting just before an in-flight trader profit is realized. The first deposit mints `deposit - MINIMUM_LIQUIDITY` shares (the Uniswap V2 convention) so the share supply never starts at a dust amount. ### Reserved liquidity -So a winning trader can always be paid, the pool **reserves** liquidity to back each open position's maximum recoverable profit (its notional `size`). An open is allowed only while `reserved + size <= liquidity`, which doubles as an open-interest cap. `close_position` caps a winner's payout at the reserved `size` (for a long, profit is capped on a more-than-doubling move; a short's profit is naturally within `size`), and provider withdrawals can take only the *free* remainder (`liquidity - reserved`). This is the simplified, single-collateral form of the reserve accounting in `solana-labs/perpetuals`. The reserve covers price profit only — funding owed *to* a position (the lighter side receives funding) is not reserved, so in the extreme a payout the pool cannot cover makes the close fail closed (revert) rather than leave the pool insolvent. +So a winning trader can always be paid, the pool **reserves** liquidity to back each open position's maximum recoverable profit (its notional `size`). An open is allowed only while `reserved + size <= liquidity`, which doubles as an open-interest cap. `close_position` caps a winner's payout at the reserved `size` (for a long, profit is capped on a more-than-doubling move; a short's profit is naturally within `size`), and provider withdrawals can take only the *free* remainder (`liquidity - reserved`). This is the simplified, single-collateral form of the reserve accounting in `solana-labs/perpetuals`. The reserve covers price profit only: funding owed *to* a position (the lighter side receives funding) is not reserved, so in the extreme a payout the pool cannot cover makes the close fail closed (revert) rather than leave the pool insolvent. ### Funding @@ -46,15 +44,15 @@ So a winning trader can always be paid, the pool **reserves** liquidity to back ### Maintenance margin and liquidation -A position's *equity* is its net collateral plus profit/loss minus funding. Once equity falls to or below the [maintenance margin](https://www.investopedia.com/terms/m/maintenancemargin.asp) (`maintenance_margin_bps` of notional), the position can be [liquidated](https://www.investopedia.com/terms/l/liquidation.asp). Liquidation is permissionless — anyone can crank it and earn the liquidation fee. +A position's *equity* is its net collateral plus profit/loss minus funding. Once equity falls to or below the [maintenance margin](https://www.investopedia.com/terms/m/maintenancemargin.asp) (`maintenance_margin_bps` of notional), the position can be [liquidated](https://www.investopedia.com/terms/l/liquidation.asp). Liquidation is permissionless: anyone can crank it and earn the liquidation fee. ### Oracle -The mark price comes from an oracle feed. This example validates the price for staleness (by slot), positivity, scale, and a [confidence band](https://docs.pyth.network/price-feeds/best-practices#confidence-intervals) that must stay within `max_confidence_bps` of the price — rejecting an uncertain price is one of the most common oracle-safety checks. +The mark price comes from an oracle feed. This example validates the price for staleness (by slot), positivity, scale, and a [confidence band](https://docs.pyth.network/price-feeds/best-practices#confidence-intervals) that must stay within `max_confidence_bps` of the price: rejecting an uncertain price is one of the most common oracle-safety checks. ### Fees and slippage -Open and close fees are charged in [basis points](https://www.investopedia.com/terms/b/basispoint.asp) (1 bp = 0.01%) of notional and accrue to the protocol. Every state-changing handler takes a `minimum_*` / acceptable-price bound — protection against [slippage](https://www.investopedia.com/terms/s/slippage.asp), the gap between the expected and actual fill — and reverts if the bound is breached. Pass `0` to opt out. +Open and close fees are charged in [basis points](https://www.investopedia.com/terms/b/basispoint.asp) (1 bp = 0.01%) of notional and accrue to the protocol. Every state-changing handler takes a `minimum_*` / acceptable-price bound (protection against [slippage](https://www.investopedia.com/terms/s/slippage.asp), the gap between the expected and actual fill) and reverts if the bound is breached. Pass `0` to opt out. --- @@ -62,51 +60,45 @@ Open and close fees are charged in [basis points](https://www.investopedia.com/t ### Participants -| Person | Role | Motivation | -|--------|------|-----------| -| **Admin** | Pool authority | Operate the market and collect the protocol's slice of trading fees. | -| **Carol** | Liquidity provider | Earn fees by funding the pool and being the counterparty to traders. | -| **Alice** | Long trader | She has a thesis that NVDA will rise and wants leveraged upside without buying the stock. | -| **Bob** | Short trader | He thinks NVDA will fall and wants to profit from the downside. | -| **Dave** | Liquidator | Runs a bot that closes under-margined positions to earn the liquidation fee. | +- **Admin** (Pool authority): Operate the market and collect the protocol's slice of trading fees. +- **Carol** (Liquidity provider): Earn fees by funding the pool and being the counterparty to traders. +- **Alice** (Long trader): She has a thesis that NVDA will rise and wants leveraged upside without buying the stock. +- **Bob** (Short trader): He thinks NVDA will fall and wants to profit from the downside. +- **Dave** (Liquidator): Runs a bot that closes under-margined positions to earn the liquidation fee. -Amounts below are shown in whole USDC; on-chain they are base units (× 10⁶). The pool is configured with 10× max leverage, 0.1% open/close fees, a 5% maintenance margin, a 1% liquidation fee, and a 1% maximum oracle confidence band. +Amounts below are shown in whole USDC; onchain they are base units (× 10⁶). The pool is configured with 10× max leverage, 0.1% open/close fees, a 5% maintenance margin, a 1% liquidation fee, and a 1% maximum oracle confidence band. --- -### Step 1 — Admin opens the market +### Step 1: Admin opens the market **Instruction:** `initialize_pool(parameters)` **Accounts created:** -| Account | Seeds / Derivation | What it stores | -|---------|--------------------|----------------| -| `Pool` [PDA](https://solana.com/docs/terminology#program-derived-address-pda) | `["pool", collateral_mint, oracle_feed]` | parameters, liquidity, reserved liquidity, collateral total, per-side open-interest accumulators, funding index, protocol fees | -| `pool_authority` PDA | `["authority", pool]` | nothing; signs vault and mint CPIs | -| `custody_vault` [token account](https://solana.com/docs/terminology#token-account) PDA | `["vault", pool]` | all USDC — both provider liquidity and trader collateral | -| `lp_mint` PDA | `["lp_mint", pool]` | the share [mint](https://solana.com/docs/terminology#mint-account); `pool_authority` is the mint authority | +- `Pool` [PDA](https://solana.com/docs/terminology#program-derived-address-pda), seeds `["pool", collateral_mint, oracle_feed]`: parameters, liquidity, reserved liquidity, collateral total, per-side open-interest accumulators, funding index, protocol fees +- `pool_authority` PDA, seeds `["authority", pool]`: nothing; signs vault and mint CPIs +- `custody_vault` [token account](https://solana.com/docs/terminology#token-account) PDA, seeds `["vault", pool]`: all USDC, both provider liquidity and trader collateral +- `lp_mint` PDA, seeds `["lp_mint", pool]`: the share [mint](https://solana.com/docs/terminology#mint-account); `pool_authority` is the mint authority --- -### Step 2 — Carol provides liquidity +### Step 2: Carol provides liquidity **Instruction:** `add_liquidity(amount = 100_000 USDC, minimum_shares_out)` **Accounts modified:** -| Account | Change | -|---------|--------| -| `carol_usdc` | −100,000 USDC | -| `custody_vault` | +100,000 USDC | -| `lp_mint` → `carol_lp` (created) | mints ≈100,000 shares to Carol | -| `Pool.liquidity` | 0 → 100,000 | +- `carol_usdc`: −100,000 USDC +- `custody_vault`: +100,000 USDC +- `lp_mint` → `carol_lp` (created): mints ≈100,000 shares to Carol +- `Pool.liquidity`: 0 → 100,000 The pool can now pay trader winnings, and Carol holds shares representing her slice of it. --- -### Step 3 — Alice opens a 5× long +### Step 3: Alice opens a 5× long **Instruction:** `open_position(side = Long, collateral_amount = 1,000 USDC, size = 5,000 USDC, acceptable_price)` @@ -114,19 +106,17 @@ NVDAx is at $100. The 0.1% open fee ($5) comes out of her collateral, leaving $9 **Accounts modified:** -| Account | Change | -|---------|--------| -| `Position` PDA `["position", pool, alice, Long]` (created) | side Long, collateral $995, size $5,000, entry price $100 | -| `alice_usdc` | −1,000 USDC | -| `custody_vault` | +1,000 USDC | -| `Pool.total_collateral` | +$995 | -| `Pool.protocol_fees` | +$5 | -| `Pool.reserved_liquidity` | +$5,000 (must stay ≤ liquidity) | -| `Pool` long open-interest accumulators | += this position | +- `Position` PDA `["position", pool, alice, Long]` (created): side Long, collateral $995, size $5,000, entry price $100 +- `alice_usdc`: −1,000 USDC +- `custody_vault`: +1,000 USDC +- `Pool.total_collateral`: +$995 +- `Pool.protocol_fees`: +$5 +- `Pool.reserved_liquidity`: +$5,000 (must stay ≤ liquidity) +- `Pool` long open-interest accumulators: += this position --- -### Step 4 — Bob opens a 5× short +### Step 4: Bob opens a 5× short **Instruction:** `open_position(side = Short, collateral_amount = 1,000 USDC, size = 5,000 USDC, acceptable_price)` @@ -136,7 +126,7 @@ While both are open, **funding** accrues to the pool from the heavier side; it i --- -### Step 5 — NVDA rises to $116. Alice closes in profit +### Step 5: NVDA rises to $116. Alice closes in profit **Instruction:** `close_position(minimum_payout)` @@ -144,19 +134,17 @@ Her profit is `5,000 × (116 − 100) / 100 = $800` (well under the $5,000 reser **Accounts modified:** -| Account | Change | -|---------|--------| -| `Pool.liquidity` | −$800 (providers pay her profit) | -| `Pool.reserved_liquidity` | −$5,000 (reserve released) | -| `Pool.total_collateral` | −$995 | -| `Pool.protocol_fees` | +$5 | -| long open-interest accumulators | −= this position | -| `custody_vault` → `alice_usdc` | pays out $1,790 (net collateral + profit − close fee) | -| `Position` (Alice) | closed; rent returned to Alice | +- `Pool.liquidity`: −$800 (providers pay her profit) +- `Pool.reserved_liquidity`: −$5,000 (reserve released) +- `Pool.total_collateral`: −$995 +- `Pool.protocol_fees`: +$5 +- long open-interest accumulators: −= this position +- `custody_vault` → `alice_usdc`: pays out $1,790 (net collateral + profit − close fee) +- `Position` (Alice): closed; rent returned to Alice --- -### Step 6 — Bob's short is underwater. Dave liquidates it +### Step 6: Bob's short is underwater. Dave liquidates it **Instruction:** `liquidate_position()` @@ -164,19 +152,17 @@ At $116 Bob's short has lost $800; his equity ($995 − $800 = $195) has fallen **Accounts modified:** -| Account | Change | -|---------|--------| -| short open-interest accumulators | −= Bob's position | -| `Pool.reserved_liquidity` | −$5,000 (reserve released) | -| `Pool.total_collateral` | −$995 | -| `Pool.liquidity` | +$800 (the loss accrues to providers) | -| `custody_vault` → `dave_usdc` (created) | $50 liquidation fee | -| `custody_vault` → `bob_usdc` | $145 remaining equity refunded | -| `Position` (Bob) | closed; rent returned to Bob | +- short open-interest accumulators: −= Bob's position +- `Pool.reserved_liquidity`: −$5,000 (reserve released) +- `Pool.total_collateral`: −$995 +- `Pool.liquidity`: +$800 (the loss accrues to providers) +- `custody_vault` → `dave_usdc` (created): $50 liquidation fee +- `custody_vault` → `bob_usdc`: $145 remaining equity refunded +- `Position` (Bob): closed; rent returned to Bob --- -### Step 7 — Admin collects the protocol's fees +### Step 7: Admin collects the protocol's fees **Instruction:** `collect_fees()` @@ -184,11 +170,11 @@ At $116 Bob's short has lost $800; his equity ($995 − $800 = $195) has fallen --- -### Step 8 — Carol withdraws +### Step 8: Carol withdraws **Instruction:** `remove_liquidity(shares, minimum_amount_out)` -Carol burns her shares and redeems USDC. Her balance now reflects the fees the pool earned plus the net of traders' wins and losses while she was in. She can withdraw only the *free* liquidity — while a position is open, the part backing it is reserved and cannot be pulled out. +Carol burns her shares and redeems USDC. Her balance now reflects the fees the pool earned plus the net of traders' wins and losses while she was in. She can withdraw only the *free* liquidity: while a position is open, the part backing it is reserved and cannot be pulled out. **Accounts modified:** `lp_mint` burns Carol's shares; `Pool.liquidity` falls; `custody_vault` pays out USDC to `carol_usdc`. @@ -198,9 +184,9 @@ Carol burns her shares and redeems USDC. Her balance now reflects the fees the p The genuinely hard part of a perpetual-futures venue is keeping it solvent and permissionless *without* re-evaluating the entire market on every action. For a rigorous, formally-verified (Kani) treatment, see Anatoly Yakovenko's [percolator](https://github.com/aeyakovenko/percolator), an educational perp risk engine. It states three invariants this example also leans on, in simplified form: -- **Realizable credit** — "protected principal is senior, positive PnL is junior, and source-domain positive credit cannot exceed realizable backing reserved for that domain." Here, provider capital is senior and trader profit is a junior claim against it: shares are priced against marked assets-under-management, and the pool reserves each position's payout up front (capping recoverable profit at the reserve) so a winner's price profit can always be paid. -- **Account-local safety** — "every favorable action refreshes the account's full active portfolio first; … stale … legs fail closed." Here, every position and liquidity action reads a fresh oracle (stale or wide-confidence prices are rejected) and recomputes pool exposure before any payout. -- **Bounded progress** — "no public instruction needs to evaluate the whole market." Here, assets-under-management comes from running per-side accumulators, and liquidation acts on one position at a time, so no handler's cost grows with the number of open positions. +- **Realizable credit**: "protected principal is senior, positive PnL is junior, and source-domain positive credit cannot exceed realizable backing reserved for that domain." Here, provider capital is senior and trader profit is a junior claim against it: shares are priced against marked assets-under-management, and the pool reserves each position's payout up front (capping recoverable profit at the reserve) so a winner's price profit can always be paid. +- **Account-local safety**: "every favorable action refreshes the account's full active portfolio first; … stale … legs fail closed." Here, every position and liquidity action reads a fresh oracle (stale or wide-confidence prices are rejected) and recomputes pool exposure before any payout. +- **Bounded progress**: "no public instruction needs to evaluate the whole market." Here, assets-under-management comes from running per-side accumulators, and liquidation acts on one position at a time, so no handler's cost grows with the number of open positions. What production pool-perps (`solana-labs/perpetuals`) add that this example still leaves out: multi-asset custody with reserves in the payout token, utilization-based borrow fees, auto-deleveraging (ADL) and an insurance fund for the bad-debt tail, and using the oracle's EMA for a less manipulable mark. @@ -212,7 +198,7 @@ This is a teaching example, not an audited exchange. Notably: - A single position per side per trader, and one collateral token per pool. - Recoverable profit is capped at the reserved notional, so the cap binds on a more-than-doubling move; a production venue would let profit run and absorb extreme moves with ADL, an insurance fund, and bankruptcy-residual accounting. -- The liquidation reward is paid from the position's remaining equity, so a position that gaps straight through zero equity pays the liquidator nothing — production venues fund the reward from collateral or an insurance fund so the worst positions are still worth liquidating. +- The liquidation reward is paid from the position's remaining equity, so a position that gaps straight through zero equity pays the liquidator nothing: production venues fund the reward from collateral or an insurance fund so the worst positions are still worth liquidating. - Funding is a single time-decay index on the heavier side rather than a skew-weighted rate. --- diff --git a/finance/perpetual-futures/quasar/README.md b/finance/perpetual-futures/quasar/README.md index 5c6a21a3..b9e7ed98 100644 --- a/finance/perpetual-futures/quasar/README.md +++ b/finance/perpetual-futures/quasar/README.md @@ -2,7 +2,7 @@ A [Quasar](https://quasar-lang.com/docs) port of the perpetual-futures example. The design, math, and behaviour match the Anchor implementation at -[`../anchor`](../anchor) — read that README for the full walkthrough of the +[`../anchor`](../anchor). Read that README for the full walkthrough of the oracle-priced, pool-collateralized model, the funding mechanism, and the money math. This page only covers what differs in the Quasar version. diff --git a/finance/token-fundraiser/kani-proofs/README.md b/finance/token-fundraiser/kani-proofs/README.md index 21985031..48837ed5 100644 --- a/finance/token-fundraiser/kani-proofs/README.md +++ b/finance/token-fundraiser/kani-proofs/README.md @@ -1,4 +1,4 @@ -# Token-fundraiser — Kani proofs +# Token-fundraiser: Kani proofs Formal-verification harnesses for the token-fundraiser program, in the spirit of [`aeyakovenko/percolator`](https://github.com/aeyakovenko/percolator), which @@ -12,11 +12,9 @@ deadline, every contributor reclaims their exact stake. Token movement is via SPL CPIs Kani cannot symbolically execute, but the accounting (`contribute`, `refund`) is pure integer arithmetic: -| Harness | Property | -| --- | --- | -| `proof_contribution_cap_bounds` | The per-contributor cap never exceeds the goal, and the `cumulative <= cap` check keeps every contributor at or below it (and below the goal). | -| `proof_current_amount_is_sum_of_contributions` | `current_amount` always equals the sum of the contributions added to it — no accounting drift. | -| `proof_refunds_sum_to_current_amount` | On a failed raise, refunds sum back to `current_amount`; no contributor reclaims more than they put in. | +- `proof_contribution_cap_bounds`: The per-contributor cap never exceeds the goal, and the `cumulative <= cap` check keeps every contributor at or below it (and below the goal). +- `proof_current_amount_is_sum_of_contributions`: `current_amount` always equals the sum of the contributions added to it, no accounting drift. +- `proof_refunds_sum_to_current_amount`: On a failed raise, refunds sum back to `current_amount`; no contributor reclaims more than they put in. The cap proof verifies nonlinear arithmetic (`goal · pct / scaler`) and uses bounded model checking; the two accounting/refund proofs are pure linear logic diff --git a/finance/token-swap/README.md b/finance/token-swap/README.md index 79e0e537..caa7c22d 100644 --- a/finance/token-swap/README.md +++ b/finance/token-swap/README.md @@ -299,7 +299,7 @@ Math: - New effective B: `(117.844 × 610.9945) / 119.9876 ≈ 600.07`. - USDC out: `610.9945 − 600.07 ≈ 10.92`. -Dave paid ~10.75 USDC offchain for 2.15 NVDAx, sold into the pool for ~10.92 USDC. Profit ~0.17 USDC, minus gas. +Dave paid ~10.75 USDC offchain for 2.15 NVDAx, sold into the pool for ~10.92 USDC. Profit ~0.17 USDC, minus transaction fees. NVDAx/USDC pool state: **119.987 NVDAx, 600.07 USDC raw**, with `admin_fees_owed_a ≈ 0.001075` and `admin_fees_owed_b ≈ 0.0055`. Mid-price on the effective reserves back to ~5.00 - *because* that's the price at which Dave's profit hit zero and he stopped. diff --git a/finance/token-swap/kani-proofs/README.md b/finance/token-swap/kani-proofs/README.md index eadb60ca..56a7736d 100644 --- a/finance/token-swap/kani-proofs/README.md +++ b/finance/token-swap/kani-proofs/README.md @@ -1,4 +1,4 @@ -# Token-swap (AMM) — Kani proofs +# Token-swap (AMM): Kani proofs Formal-verification harnesses for the constant-product AMM, in the spirit of [`aeyakovenko/percolator`](https://github.com/aeyakovenko/percolator), which @@ -8,27 +8,25 @@ the mathematical correctness of a DeFi engine. ## What is verified The on-chain instructions hand token movement to the SPL token program through -CPIs that Kani cannot symbolically execute, but the *interesting* part — the +CPIs that Kani cannot symbolically execute, but the *interesting* part, the constant-product curve, the fee split, the integer square root used for the -initial LP mint, and the proportional-withdraw math — is pure integer +initial LP mint, and the proportional-withdraw math, is pure integer arithmetic. This crate reproduces those formulas faithfully (same `u128` widening, multiply-before-divide, floor rounding) and proves their invariants: -| Harness | Property | -| --- | --- | -| `proof_fee_split_bounds` | `fee <= input`, `admin_portion <= fee`, and `taxed_input + fee == input`. | -| `proof_swap_preserves_constant_product` | **The core safety property**: a swap never decreases `k = reserve_in * reserve_out`. | -| `proof_swap_cannot_fully_drain_when_reserve_positive` | With a non-empty input reserve, output is always `< other_reserve` (pool stays solvent). | -| `proof_swap_at_zero_reserve_drains_whole_pool` | **Finding**, proven as a positive characterization (see below). | -| `proof_integer_sqrt_is_floor` | `integer_sqrt` returns the exact floor: `r² <= n < (r+1)²`. | -| `proof_withdraw_never_exceeds_reserve` | An LP can never withdraw more than the reserve holds (the `MINIMUM_LIQUIDITY` floor guarantees it). | -| `proof_deposit_clamp_never_exceeds_request` | The ratio clamp never spends more of either token than the caller offered. | +- `proof_fee_split_bounds`: `fee <= input`, `admin_portion <= fee`, and `taxed_input + fee == input`. +- `proof_swap_preserves_constant_product`: **The core safety property**: a swap never decreases `k = reserve_in * reserve_out`. +- `proof_swap_cannot_fully_drain_when_reserve_positive`: With a non-empty input reserve, output is always `< other_reserve` (pool stays solvent). +- `proof_swap_at_zero_reserve_drains_whole_pool`: **Finding**, proven as a positive characterization (see below). +- `proof_integer_sqrt_is_floor`: `integer_sqrt` returns the exact floor: `r² <= n < (r+1)²`. +- `proof_withdraw_never_exceeds_reserve`: An LP can never withdraw more than the reserve holds (the `MINIMUM_LIQUIDITY` floor guarantees it). +- `proof_deposit_clamp_never_exceeds_request`: The ratio clamp never spends more of either token than the caller offered. ## Bounded model checking Several harnesses verify **nonlinear 128-bit arithmetic** (e.g. `reserve_in * reserve_out`, and worst of all `amount * pool_b / pool_a` where -the *divisor* is symbolic), the hardest case for a bit-precise model checker — +the *divisor* is symbolic), the hardest case for a bit-precise model checker. Kani bit-blasts the full multiplier/divider into SAT. Following percolator's own practice (it bounds inputs to ranges like `±500`), these harnesses constrain their symbolic inputs to a representative range so the solver stays fast. The @@ -36,15 +34,13 @@ identities being proven are scale-invariant, so the bounded domain still exercises every rounding boundary. The bound is per-harness, sized to its difficulty: -| Harness | Input bound | Time | -| --- | --- | --- | -| `proof_fee_split_bounds` | `input <= 4095`, fractions fully symbolic | ~2s | -| `proof_swap_preserves_constant_product` | reserves/input `<= 63` | ~26s | -| `proof_swap_cannot_fully_drain_when_reserve_positive` | reserves/input `<= 255` | ~7s | -| `proof_swap_at_zero_reserve_drains_whole_pool` | `<= 255` | ~15s | -| `proof_integer_sqrt_is_floor` | `n <= 255`, `unwind(11)` | ~33s | -| `proof_withdraw_never_exceeds_reserve` | `<= 4095` | ~5s | -| `proof_deposit_clamp_never_exceeds_request` | `<= 31` (symbolic divisor) | ~3s | +- `proof_fee_split_bounds`: `input <= 4095`, fractions fully symbolic, runs in ~2s +- `proof_swap_preserves_constant_product`: reserves/input `<= 63`, runs in ~26s +- `proof_swap_cannot_fully_drain_when_reserve_positive`: reserves/input `<= 255`, runs in ~7s +- `proof_swap_at_zero_reserve_drains_whole_pool`: `<= 255`, runs in ~15s +- `proof_integer_sqrt_is_floor`: `n <= 255`, `unwind(11)`, runs in ~33s +- `proof_withdraw_never_exceeds_reserve`: `<= 4095`, runs in ~5s +- `proof_deposit_clamp_never_exceeds_request`: `<= 31` (symbolic divisor), runs in ~3s The whole suite verifies in ~90s of solver time. This is why these proofs run **weekly in CI** (the `kani.yml` `verify` job), not on every push/PR. A fast @@ -56,7 +52,7 @@ unit-test job runs per push/PR. bound is tight: when the input-side *effective* reserve is exactly `0`, the constant-product curve outputs the **entire** opposite reserve (`output == other_reserve`), draining that side to zero. The end-of-swap -`require!(new_invariant >= invariant)` guard does **not** catch it — with +`require!(new_invariant >= invariant)` guard does **not** catch it, with `this_reserve == 0` the pre-trade product `k = 0 * other_reserve = 0`, so the post-trade product (also `0`) trivially satisfies `0 >= 0`. @@ -74,10 +70,10 @@ so the drained state is unreachable on-chain regardless of any reachability argument. Reaching `effective_reserve == 0` was already a degenerate state the deposit path prevents (the `MINIMUM_LIQUIDITY` floor keeps the bootstrap product positive, and `proof_swap_preserves_constant_product` shows ordinary swaps keep -both sides positive), so this was a latent edge, not a live exploit — but the +both sides positive), so this was a latent edge, not a live exploit, but the guard means solvency no longer *depends* on that argument. -The harness is kept as a **positive** proof (every assertion holds — `output == +The harness is kept as a **positive** proof (every assertion holds: `output == other_reserve` and `0 >= 0`) characterizing the raw `swap_output` formula at the boundary, which is exactly the justification for the program guard. It is not a `#[kani::should_panic]`, which would have started failing the moment the diff --git a/finance/vault-strategy/anchor/README.md b/finance/vault-strategy/anchor/README.md index ed91092e..63f1b025 100644 --- a/finance/vault-strategy/anchor/README.md +++ b/finance/vault-strategy/anchor/README.md @@ -10,10 +10,8 @@ A note on the word **vault**: by the common standard (ERC-4626) a vault holds a ## Programs -| Program | Description | -|---------|-------------| -| `vault-strategy` | Registry/whitelist, strategy creation, asset registration, deposits, share minting, fee accrual, rebalancing, withdrawals | -| `mock-swap-router` | Test-only fake Jupiter. Stores exchange rates, mints/burns basket tokens for USDC. Replaced by real [Jupiter](https://jup.ag) in production. | +- **`vault-strategy`**: Registry/whitelist, strategy creation, asset registration, deposits, share minting, fee accrual, rebalancing, withdrawals +- **`mock-swap-router`**: Test-only fake Jupiter. Stores exchange rates, mints/burns basket tokens for USDC. Replaced by real [Jupiter](https://jup.ag) in production. --- diff --git a/finance/vault-strategy/kani-proofs/README.md b/finance/vault-strategy/kani-proofs/README.md index 79f17b85..ed982322 100644 --- a/finance/vault-strategy/kani-proofs/README.md +++ b/finance/vault-strategy/kani-proofs/README.md @@ -1,4 +1,4 @@ -# Vault-strategy — Kani proofs +# Vault-strategy: Kani proofs Formal-verification harnesses for the ERC4626-style share vault, in the spirit of [`aeyakovenko/percolator`](https://github.com/aeyakovenko/percolator), which @@ -12,23 +12,19 @@ burn shares for a proportional slice of every vault balance; a manager fee mints a small slice of shares over time. Token movement is via SPL CPIs Kani cannot symbolically execute, but the share math is pure integer arithmetic: -| Harness | Property | -| --- | --- | -| `proof_withdraw_within_balance` | **Solvency**: a withdrawal never takes more of any vault balance than it holds (`floor(balance·shares/total) <= balance`, since `shares <= total`); burning the whole supply takes exactly the whole balance. | -| `proof_deposit_withdraw_cannot_extract` | A deposit→withdraw round-trip never returns more than was deposited — no rounding attack mints shares worth more than they cost. | -| `proof_fee_shares_bounded_by_supply` | The time-based manager fee can never mint more than 100%/year of dilution (`fee_shares <= total_shares` for `elapsed <= 1yr`, `fee_bps <= 10000`). | +- `proof_withdraw_within_balance`: **Solvency**: a withdrawal never takes more of any vault balance than it holds (`floor(balance·shares/total) <= balance`, since `shares <= total`); burning the whole supply takes exactly the whole balance. +- `proof_deposit_withdraw_cannot_extract`: A deposit→withdraw round-trip never returns more than was deposited, no rounding attack mints shares worth more than they cost. +- `proof_fee_shares_bounded_by_supply`: The time-based manager fee can never mint more than 100%/year of dilution (`fee_shares <= total_shares` for `elapsed <= 1yr`, `fee_bps <= 10000`). ## Bounded model checking All three verify nonlinear 128-bit arithmetic with a symbolic divisor (the share -supply / NAV), so — as percolator does — they bound their symbolic inputs to a +supply / NAV), so (as percolator does) they bound their symbolic inputs to a representative range; the share identities are scale-invariant. -| Harness | Bound | Time | -| --- | --- | --- | -| `proof_withdraw_within_balance` | balances/supply `<= 255` | ~12s | -| `proof_deposit_withdraw_cannot_extract` | `<= 31` | ~3s | -| `proof_fee_shares_bounded_by_supply` | `<= 255` | ~4s | +- `proof_withdraw_within_balance`: balances/supply `<= 255`, runs in ~12s +- `proof_deposit_withdraw_cannot_extract`: `<= 31`, runs in ~3s +- `proof_fee_shares_bounded_by_supply`: `<= 255`, runs in ~4s Run weekly in CI (the `kani.yml` `verify` job), not on every push/PR, because the bounded nonlinear proofs are slow. A fast unit-test job runs per push/PR. From 8dbc7dcff3f17a875f7b7c3bd3fe9c23d42323c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 14:32:38 +0000 Subject: [PATCH 3/5] docs(finance): convert tables to prose in order-book and lending READMEs Completes the finance README pass: order-book/anchor (16 tables), and the lending anchor/quasar READMEs (em-dashes). Wide tables are folded into prose bullets with labels drawn verbatim from the original column headers; all cell content, identifiers, numbers, and links preserved. Also swaps the two em-dashes in a lending/anchor shell-comment for hyphens. --- finance/lending/anchor/README.md | 36 ++--- finance/lending/quasar/README.md | 24 ++-- finance/order-book/anchor/README.md | 204 ++++++++++++---------------- 3 files changed, 116 insertions(+), 148 deletions(-) diff --git a/finance/lending/anchor/README.md b/finance/lending/anchor/README.md index 3bc967cc..a6a131a4 100644 --- a/finance/lending/anchor/README.md +++ b/finance/lending/anchor/README.md @@ -3,7 +3,7 @@ A Kamino/Solend-style borrow/lend program: suppliers earn interest on deposits, borrowers post collateral and draw other assets against it, and liquidators keep the market solvent. It demonstrates the techniques the most-used Solana lending -protocols share — share-token deposit accounting, a utilization-based interest +protocols share: share-token deposit accounting, a utilization-based interest index, oracle-priced obligation health, and close-factor-capped liquidation. ## Purpose @@ -31,38 +31,38 @@ crosses the liquidation threshold and a liquidator can close part of the positio ### Accounts -- **`LendingMarket`** — top-level config (owner, quote-currency mint). PDA seeds +- **`LendingMarket`**: top-level config (owner, quote-currency mint). PDA seeds `["lending_market", market_id]`, where `market_id` is a `u64` index. Seeding by an index alone (owner is stored as a field for authorization, not baked into the - address) lets one owner run several independent, risk-isolated markets — their - market 0, 1, 2 … — with no cross-owner collisions and no individual's key in a + address) lets one owner run several independent, risk-isolated markets (their + market 0, 1, 2 …) with no cross-owner collisions and no individual's key in a shared struct's address. -- **`Reserve`** — one per asset. Owns a program-controlled liquidity vault and a +- **`Reserve`**: one per asset. Owns a program-controlled liquidity vault and a share-token mint, and stores the interest-rate config, the cumulative borrow- rate index, available liquidity, and scaled total debt. PDA seeds `["reserve", market, liquidity_mint]`. -- **`Obligation`** — one per borrower per market: the share-token collateral +- **`Obligation`**: one per borrower per market: the share-token collateral posted and the liquidity borrowed, with cached quote-currency valuations. PDA seeds `["obligation", market, owner]`. -- **`PriceFeed`** — a price for one token (see Oracle below). +- **`PriceFeed`**: a price for one token (see Oracle below). ### Share tokens (the deposit claim) Supplying liquidity mints share tokens; redeeming burns them. The exchange rate is `total_liquidity / share_supply`, where `total_liquidity = available_liquidity + current_debt`. `available_liquidity` (not the vault's raw token balance) is the -source of truth, so a token donated directly to the vault cannot inflate the rate -— closing the classic empty-pool inflation attack. The first deposit mints 1:1. +source of truth, so a token donated directly to the vault cannot inflate the rate, +closing the classic empty-pool inflation attack. The first deposit mints 1:1. ### Interest: a kinked curve and a cumulative index Each `refresh_reserve` advances `cumulative_borrow_rate_index` by `(1 + rate_per_slot * elapsed_slots)`. `rate_per_slot` comes from a kinked -utilization curve — linear from `min_borrow_rate_bps` to `optimal_borrow_rate_bps` +utilization curve: linear from `min_borrow_rate_bps` to `optimal_borrow_rate_bps` up to `optimal_utilization_bps`, then steeper to `max_borrow_rate_bps` at full utilization. Each borrow stores its principal as **scaled debt** (principal ÷ index at borrow time), so every obligation's debt grows automatically as the -index advances — no per-obligation accrual loop. +index advances: no per-obligation accrual loop. ### Protocol fees (how the market earns) @@ -97,7 +97,7 @@ less, which would make the liquidator overpay. ### Fixed-point math -All money math is integer-only `u128` — no floats, no fixed-point crates. Ratios +All money math is integer-only `u128`: no floats, no fixed-point crates. Ratios (rates, the index, the exchange rate, obligation values) are scaled by `FIXED_POINT_SCALE` (10^18). Every conversion rounds in the protocol's favour (user output floored, debt ceiled), so dust cannot be extracted by repeated @@ -108,8 +108,8 @@ round-trips. `PriceFeed` mirrors a Switchboard On-Demand pull feed: a signed mantissa, an exponent (`price = mantissa * 10^exponent`), and the slot the price was written. Freshness is checked in **slots** (`MAX_PRICE_STALENESS_SLOTS`), not wall-clock -time. The feed PDA is seeded by `[b"price_feed", market, mint]` — scoped to a -market, not to any individual — and only that market's `owner` may write it +time. The feed PDA is seeded by `[b"price_feed", market, mint]` (scoped to a +market, not to any individual) and only that market's `owner` may write it (`set_price` checks `has_one = owner`). So prices can't be squatted, a reserve trusts exactly its own market's feed for the mint, and isolated markets can price the same asset independently. @@ -127,7 +127,7 @@ Supplied liquidity sits in program-owned vault PDAs, and posted collateral sits per-obligation vault PDAs whose authority is the obligation PDA. The market owner can update reserve risk parameters (`update_reserve_config`) and withdraw the protocol's earned fees (`collect_protocol_fees`), but has no path to a supplier's -deposits or a borrower's collateral — there is no admin escape hatch over user funds. +deposits or a borrower's collateral: there is no admin escape hatch over user funds. ### Known limits @@ -164,12 +164,12 @@ refreshed in the same transaction, so a typical action transaction is ## Testing ```sh -anchor build # or: cargo build-sbf — produces target/deploy/lending.so -anchor test # or: cargo test — runs the LiteSVM integration tests +anchor build # or: cargo build-sbf - produces target/deploy/lending.so +anchor test # or: cargo test - runs the LiteSVM integration tests ``` `anchor build` (or `cargo build-sbf`) must run first: the tests load the compiled `target/deploy/lending.so` via `include_bytes!`. The suite covers the -non-happy-path branches — interest accrual, borrowing at the LTV limit, stale +non-happy-path branches: interest accrual, borrowing at the LTV limit, stale reserve/price rejection, liquidation of an unhealthy obligation after a price move, the share-inflation guard, and rounding edges. diff --git a/finance/lending/quasar/README.md b/finance/lending/quasar/README.md index a27527a0..76d5a214 100644 --- a/finance/lending/quasar/README.md +++ b/finance/lending/quasar/README.md @@ -9,8 +9,8 @@ close-factor liquidation with a bonus. ## What's different from the Anchor version Quasar accounts are fixed-size and zero-copy. Quasar *does* support bounded -collections (`Vec` / `PodVec`) and remaining accounts (`CtxWithRemaining`) — -the `multisig` example uses both — so a multi-asset obligation is expressible. But +collections (`Vec` / `PodVec`) and remaining accounts (`CtxWithRemaining`), +the `multisig` example uses both, so a multi-asset obligation is expressible. But the shipped Quasar DeFi examples (`escrow`, `vault`) model one position with fixed-size accounts, so this port follows that idiom: @@ -28,39 +28,39 @@ Everything else mirrors the Anchor version. ## Major concepts -- **`LendingMarket`** — market config (owner, quote-currency mint). PDA: +- **`LendingMarket`**: market config (owner, quote-currency mint). PDA: `["lending_market", market_id]`, where `market_id` is a `u64` index. Owner is stored as a field for authorization, not baked into the address, so one owner can run several isolated markets (their market 0, 1, 2 …) with no individual's key in a shared struct's address. -- **`Reserve`** — one asset's pool. Owns a program-controlled liquidity vault and +- **`Reserve`**: one asset's pool. Owns a program-controlled liquidity vault and a share-token mint (both PDAs, authority = the reserve), and stores the interest-rate config, the cumulative borrow-rate index, available liquidity, and scaled total debt. PDA: `["reserve", market, liquidity_mint]`. -- **`Obligation`** — a borrower's isolated position: the collateral reserve and +- **`Obligation`**: a borrower's isolated position: the collateral reserve and deposited share amount, plus the borrow reserve and scaled debt. PDA: `["obligation", market, owner]`. -- **`PriceFeed`** — a Switchboard-On-Demand-shaped price (`mantissa * 10^exponent` - + slot). PDA: `["price_feed", market, mint]` — scoped to a market, not to any +- **`PriceFeed`**: a Switchboard-On-Demand-shaped price (`mantissa * 10^exponent` + + slot). PDA: `["price_feed", market, mint]`: scoped to a market, not to any individual; only the market's `owner` may write it, so prices can't be squatted and each market prices its own assets. `set_price` writes it directly for deterministic tests; in production a reserve points at the real Switchboard feed. Freshness is checked in slots. -- **Liquidation** — the close factor (max fraction of the debt one call repays) +- **Liquidation**: the close factor (max fraction of the debt one call repays) comes from the borrow reserve; the bonus from the collateral reserve. A repayment whose seizure would exceed the posted collateral fails with `LiquidationTooLarge` rather than silently seizing less, which would make the liquidator overpay. -- **Share tokens** — supplying mints them, redeeming burns them; the exchange rate +- **Share tokens**: supplying mints them, redeeming burns them; the exchange rate `total_liquidity / share_supply` rises as borrowers pay interest. `available_liquidity` (not the vault's raw balance) is the source of truth, so a token donation can't inflate the rate. -- **Protocol fees** — the reserve keeps `reserve_factor_bps` of each interest +- **Protocol fees**: the reserve keeps `reserve_factor_bps` of each interest accrual in `accumulated_protocol_fees` (carved out of total liquidity, so it never lifts the supplier exchange rate); the market owner withdraws it with `collect_protocol_fees`. That spread between the borrow and supply rates is how the owner earns. -- **Integer-only math** — `u128`, scaled by `FIXED_POINT_SCALE` (10^18), every +- **Integer-only math**: `u128`, scaled by `FIXED_POINT_SCALE` (10^18), every conversion rounding in the protocol's favour. ### Instruction handlers (numeric discriminators) @@ -87,6 +87,6 @@ cargo test tests:: # runs the quasar-svm integration tests `cargo build-sbf` must run first: the tests load the compiled `target/deploy/quasar_lending.so` into `quasar-svm`. The suite drives the full -lifecycle — supply/redeem (1:1 first deposit), borrow up to the LTV limit (and +lifecycle: supply/redeem (1:1 first deposit), borrow up to the LTV limit (and rejection beyond it), repay, interest accrual lifting the share value after slots pass, and liquidation of an unhealthy position (with a healthy position rejected). diff --git a/finance/order-book/anchor/README.md b/finance/order-book/anchor/README.md index 1c1e2923..c836746b 100644 --- a/finance/order-book/anchor/README.md +++ b/finance/order-book/anchor/README.md @@ -1,6 +1,6 @@ -# Order Book — Central Limit Order Book (CLOB) Exchange +# Order Book: Central Limit Order Book (CLOB) Exchange -This is an **[order book](https://www.investopedia.com/terms/o/order-book.asp)** — specifically, a **[central limit order +This is an **[order book](https://www.investopedia.com/terms/o/order-book.asp)**: specifically, a **[central limit order book (CLOB)](https://www.investopedia.com/terms/l/limitorderbook.asp)**, the standard piece of market infrastructure used by NYSE, NASDAQ, LSE, CME, and every crypto venues like Phoenix and Cube and OpenBook. An Anchor program that runs an onchain order book for a single pair of token mints: users post buy or sell offers at the prices they want, the program @@ -184,21 +184,17 @@ This section walks through a complete sequence of trades using four real partici ### The tokens -| Token | What it is | Role on this market | -|---|---|---| -| **NVDAx** | An onchain NVIDIA share (xStock). Its price tracks the underlying stock. | **Base asset** - the thing being bought and sold | -| **USDC** | A stablecoin redeemable 1:1 for US dollars | **Quote asset** - the currency used for pricing and payment | +- **NVDAx** (**Base asset** - the thing being bought and sold): An onchain NVIDIA share (xStock). Its price tracks the underlying stock. +- **USDC** (**Quote asset** - the currency used for pricing and payment): A stablecoin redeemable 1:1 for US dollars A price of **960** means "960 USDC per NVDAx". The same program logic - identical instruction handlers and account structure - works for any other pair, such as **TSLAx/USDC** (Tesla xStock). ### The participants -| Name | Role | Motivation | -|---|---|---| -| **Maria** | Market authority | Earns 0.25 % ([25 basis points](https://www.investopedia.com/terms/b/basispoint.asp)) on every fill. Her revenue scales with market volume, so she wants a liquid, trusted venue. | -| **Alice** | Retail investor - buyer | Bullish thesis: she expects NVDAx to rise from ~960 USDC to ~1 100 as demand for NVIDIA's AI chips grows. She wants to accumulate NVDAx at a good price before that move. | -| **Bob** | [Market maker](https://www.investopedia.com/terms/m/marketmaker.asp) | No directional view on NVDAx. Profits from the [bid-ask spread](https://www.investopedia.com/terms/b/bid-askspread.asp): he simultaneously quotes a buy price (bid) below fair value and a sell price (ask) above it. If both sides fill, the difference is his gross revenue. He provides [liquidity](https://www.investopedia.com/terms/l/liquidity.asp) to the market in exchange for that spread. | -| **Carol** | Retail investor - seller | Bought NVDAx at 800 USDC six months ago. It is now trading around 960. She wants to sell some to [realise her profit](https://www.investopedia.com/terms/r/realizedprofit.asp) in USDC. | +- **Maria** (Market authority): Earns 0.25 % ([25 basis points](https://www.investopedia.com/terms/b/basispoint.asp)) on every fill. Her revenue scales with market volume, so she wants a liquid, trusted venue. +- **Alice** (Retail investor - buyer): Bullish thesis: she expects NVDAx to rise from ~960 USDC to ~1 100 as demand for NVIDIA's AI chips grows. She wants to accumulate NVDAx at a good price before that move. +- **Bob** ([Market maker](https://www.investopedia.com/terms/m/marketmaker.asp)): No directional view on NVDAx. Profits from the [bid-ask spread](https://www.investopedia.com/terms/b/bid-askspread.asp): he simultaneously quotes a buy price (bid) below fair value and a sell price (ask) above it. If both sides fill, the difference is his gross revenue. He provides [liquidity](https://www.investopedia.com/terms/l/liquidity.asp) to the market in exchange for that spread. +- **Carol** (Retail investor - seller): Bought NVDAx at 800 USDC six months ago. It is now trading around 960. She wants to sell some to [realise her profit](https://www.investopedia.com/terms/r/realizedprofit.asp) in USDC. --- @@ -209,13 +205,11 @@ A price of **960** means "960 USDC per NVDAx". The same program logic - identica Maria's wallet signs. Five accounts are created: -| Account | Type | Seeds / notes | State after | -|---|---|---|---| -| `Market` PDA | Program data | `["market", NVDAx_mint, USDC_mint]` | `fee_bps=25`, `tick_size=1`, `is_active=true`; vault addresses recorded | -| `OrderBook` | Zero-copy slab (~180 KB) | Client-allocated (not a PDA) | Both critbit trees empty | -| `base_vault` | Token account (NVDAx) | Authority = Market PDA | 0 NVDAx | -| `quote_vault` | Token account (USDC) | Authority = Market PDA | 0 USDC | -| `fee_vault` | Token account (USDC) | Authority = Market PDA | 0 USDC | +- `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 **No tokens move.** Maria pays the SOL rent for all five accounts. @@ -227,11 +221,9 @@ Maria's wallet signs. Five accounts are created: Each call creates one `MarketUser` PDA - a per-(trader, market) account that tracks their open orders and any tokens owed to them: -| Account | Seeds | State after | -|---|---|---| -| Alice's `MarketUser` PDA | `["market_user", market, alice_pubkey]` | `unsettled_base=0`, `unsettled_quote=0`, `open_orders=[]` | -| Bob's `MarketUser` PDA | `["market_user", market, bob_pubkey]` | same | -| Carol's `MarketUser` PDA | `["market_user", market, carol_pubkey]` | same | +- Alice's `MarketUser` PDA: seeds `["market_user", market, alice_pubkey]`; `unsettled_base=0`, `unsettled_quote=0`, `open_orders=[]` +- Bob's `MarketUser` PDA: seeds `["market_user", market, bob_pubkey]`; same +- Carol's `MarketUser` PDA: seeds `["market_user", market, carol_pubkey]`; same --- @@ -248,12 +240,10 @@ bob_nvdax_ata --[10 NVDAx]--> base_vault **Accounts changed:** -| Account | Change | -|---|---| -| `base_vault` | +10 NVDAx | -| New `Order` PDA (id=1) | `side=Ask, price=965, qty=10, status=Open` | -| `OrderBook.asks` | Leaf inserted at price 965 | -| Bob's `MarketUser.open_orders` | `[1]` | +- `base_vault`: +10 NVDAx +- New `Order` PDA (id=1): `side=Ask, price=965, qty=10, status=Open` +- `OrderBook.asks`: Leaf inserted at price 965 +- Bob's `MarketUser.open_orders`: `[1]` **Book state:** ``` @@ -276,12 +266,10 @@ alice_usdc_ata --[950 × 5 = 4 750 USDC]--> quote_vault **Accounts changed:** -| Account | Change | -|---|---| -| `quote_vault` | +4 750 USDC | -| New `Order` PDA (id=2) | `side=Bid, price=950, qty=5, status=Open` | -| `OrderBook.bids` | Leaf inserted at price 950 | -| Alice's `MarketUser.open_orders` | `[2]` | +- `quote_vault`: +4 750 USDC +- New `Order` PDA (id=2): `side=Bid, price=950, qty=5, status=Open` +- `OrderBook.bids`: Leaf inserted at price 950 +- Alice's `MarketUser.open_orders`: `[2]` **Book state:** ``` @@ -308,25 +296,21 @@ carol_nvdax_ata --[3 NVDAx]--> base_vault **Fill accounting (fill price = 950, fill qty = 3):** -| Line item | Calculation | Result | -|---|---|---| -| Gross quote exchanged | 950 × 3 | 2 850 USDC | -| Taker fee (25 bps) | ceil(2 850 × 25 / 10 000) = ceil(7.125) | 8 USDC | -| Carol's net proceeds | 2 850 − 8 | 2 842 USDC → `carol.MarketUser.unsettled_quote` | -| Alice's base received | 3 NVDAx | → `alice.MarketUser.unsettled_base` | +- **Gross quote exchanged**: 950 × 3; 2 850 USDC +- **Taker fee (25 bps)**: ceil(2 850 × 25 / 10 000) = ceil(7.125); 8 USDC +- **Carol's net proceeds**: 2 850 − 8; 2 842 USDC → `carol.MarketUser.unsettled_quote` +- **Alice's base received**: 3 NVDAx; → `alice.MarketUser.unsettled_base` **Accounts changed:** -| Account | Change | -|---|---| -| `base_vault` | +3 NVDAx (Carol's lock) | -| `fee_vault` | +8 USDC (fee CPI from quote_vault) | -| Alice's `Order` PDA (id=2) | `filled_quantity=3`, `status=PartiallyFilled` | -| Alice's `MarketUser.unsettled_base` | +3 NVDAx | -| Alice's `MarketUser.open_orders` | `[2]` (still open - 2 of 5 NVDAx remain) | -| Carol's `MarketUser.unsettled_quote` | +2 842 USDC | -| New Carol's `Order` PDA (id=3) | `side=Ask, price=945, qty=3, status=Filled` | -| `OrderBook.bids` | Alice's leaf quantity: 5 → 2 | +- `base_vault`: +3 NVDAx (Carol's lock) +- `fee_vault`: +8 USDC (fee CPI from quote_vault) +- Alice's `Order` PDA (id=2): `filled_quantity=3`, `status=PartiallyFilled` +- Alice's `MarketUser.unsettled_base`: +3 NVDAx +- Alice's `MarketUser.open_orders`: `[2]` (still open - 2 of 5 NVDAx remain) +- Carol's `MarketUser.unsettled_quote`: +2 842 USDC +- New Carol's `Order` PDA (id=3): `side=Ask, price=945, qty=3, status=Filled` +- `OrderBook.bids`: Alice's leaf quantity: 5 → 2 **Book state:** ``` @@ -368,12 +352,10 @@ fee_vault --[8 USDC]--> maria_usdc_ata ### Final position -| Participant | Paid / locked | Received | Outcome | -|---|---|---|---| -| **Alice** | 4 750 USDC (for 5 NVDAx) | 3 NVDAx + 1 900 USDC still in `quote_vault` (2-NVDAx bid resting at 950) | Thesis running; waiting for a seller at 950 to fill the rest | -| **Carol** | 3 NVDAx (cost 800 each) | 2 842 USDC | Locked in ≈ 147 USDC/NVDAx profit net of fee | -| **Bob** | 10 NVDAx locked | Nothing yet - ask at 965 unfilled | Earns the spread when a buyer at 965 arrives | -| **Maria** | - | 8 USDC | Fee revenue | +- **Alice**: paid / locked 4 750 USDC (for 5 NVDAx); received 3 NVDAx + 1 900 USDC still in `quote_vault` (2-NVDAx bid resting at 950); outcome Thesis running; waiting for a seller at 950 to fill the rest +- **Carol**: paid / locked 3 NVDAx (cost 800 each); received 2 842 USDC; outcome Locked in ≈ 147 USDC/NVDAx profit net of fee +- **Bob**: paid / locked 10 NVDAx locked; received Nothing yet - ask at 965 unfilled; outcome Earns the spread when a buyer at 965 arrives +- **Maria**: paid / locked -; received 8 USDC; outcome Fee revenue Alice's remaining 2-NVDAx [bid](https://www.investopedia.com/terms/b/bid.asp) stays on the book. The next seller willing to part with NVDAx at 950 or below will fill it automatically. A **TSLAx/USDC** market runs the same seven steps with different mint addresses. @@ -383,20 +365,16 @@ Alice's remaining 2-NVDAx [bid](https://www.investopedia.com/terms/b/bid.asp) st ### State / data accounts -| Account | PDA? | Seeds | Authority | Holds | -|---|---|---|---|---| -| `Market` | yes | `["market", base_mint, quote_mint]` | program | fee rate, tick size, min order size, base/quote mint pubkeys, vault pubkeys, order book pubkey, `authority` wallet (allowed to withdraw fees) | -| `OrderBook` | no (client-allocated keypair) | 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` | program | two critbit trees (bids highest-first, asks lowest-first, 1024 leaves each), `next_order_id` | -| `Order` | yes | `["order", market, order_id.to_le_bytes()]` | program | owner, side, price, original_quantity, filled_quantity, status, timestamp | -| `MarketUser` | yes | `["market_user", market, owner]` | program | `unsettled_base`, `unsettled_quote`, `open_orders: Vec` (max 20) | +- `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` +- `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` (max 20) ### Token accounts (owned by the Token Program, authority = Market PDA) -| Account | PDA? | Authority | Mint | Holds | -|---|---|---|---|---| -| `base_vault` | no (regular token account) | Market PDA | base | 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` | no | Market PDA | quote | bids' locked quote pre-match, plus quote owed to ask-takers and bid-makers waiting for settlement | -| `fee_vault` | no | Market PDA | quote | taker fees accumulated across all fills; drained by `withdraw_fees` | +- `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` Note: the **token vaults are not PDAs**. They are regular token accounts created with `init` in `initialize_market.rs`; their @@ -1294,24 +1272,22 @@ imagine a future instruction handler to reclaim its rent - see §8). From [`errors.rs`](programs/order-book/src/errors.rs): -| Error | When | -|---|---| -| `InvalidPrice` | `place_order` called with `price == 0` | -| `OrderNotFound` | `cancel_order` failed to locate the order in the book (sanity path) | -| `MarketPaused` | `place_order` on a market with `is_active = false` (no handler flips this today, but the field is there) | -| `Unauthorized` | `cancel_order` by someone other than the order owner | -| `OrderBookFull` | `place_order` remainder would push the taker's side past 1024 leaves | -| `TooManyOpenOrders` | User already has 20 open orders on this market | -| `InvalidTickSize` | `tick_size == 0` at init, or `price % tick_size != 0` on place | -| `BelowMinOrderSize` | `min_order_size == 0` at init, or `quantity < min_order_size` on place | -| `OrderNotCancellable` | `cancel_order` on a Filled or Cancelled order | -| `NumericalOverflow` | Any checked arithmetic returned `None` | -| `InvalidFeeBasisPoints` | `fee_basis_points > 10_000` at init | -| `InvalidFeeVault` | `market.fee_vault` on the struct does not match the passed `fee_vault` (Anchor `has_one`) | -| `MakerAccountMismatch` | Wrong number of maker accounts, wrong order, wrong market, or caller walked the book out of order | -| `MissingMakerAccounts` | `remaining_accounts.len()` not a multiple of 2 | -| `MakerOwnerMismatch` | Maker Order and MarketUser have different owners | -| `NotMarketAuthority` | `withdraw_fees` called by wrong signer | +- `InvalidPrice`: `place_order` called with `price == 0` +- `OrderNotFound`: `cancel_order` failed to locate the order in the book (sanity path) +- `MarketPaused`: `place_order` on a market with `is_active = false` (no handler flips this today, but the field is there) +- `Unauthorized`: `cancel_order` by someone other than the order owner +- `OrderBookFull`: `place_order` remainder would push the taker's side past 1024 leaves +- `TooManyOpenOrders`: User already has 20 open orders on this market +- `InvalidTickSize`: `tick_size == 0` at init, or `price % tick_size != 0` on place +- `BelowMinOrderSize`: `min_order_size == 0` at init, or `quantity < min_order_size` on place +- `OrderNotCancellable`: `cancel_order` on a Filled or Cancelled order +- `NumericalOverflow`: Any checked arithmetic returned `None` +- `InvalidFeeBasisPoints`: `fee_basis_points > 10_000` at init +- `InvalidFeeVault`: `market.fee_vault` on the struct does not match the passed `fee_vault` (Anchor `has_one`) +- `MakerAccountMismatch`: Wrong number of maker accounts, wrong order, wrong market, or caller walked the book out of order +- `MissingMakerAccounts`: `remaining_accounts.len()` not a multiple of 2 +- `MakerOwnerMismatch`: Maker Order and MarketUser have different owners +- `NotMarketAuthority`: `withdraw_fees` called by wrong signer ### 6.2 Guarded design choices worth knowing @@ -1469,46 +1445,38 @@ test taker_partially_fills_resting_order_rest_stays_on_book ... ok **Setup / happy path (pre-matching):** -| Test | Exercises | -|---|---| -| `initialize_market_sets_market_and_order_book` | PDA creation, vault setup, initial field values | -| `create_market_user_tracks_market_and_owner` | Per-user PDA derivation and zero-initialised counters | -| `place_bid_locks_quote_in_vault` | Fund lock on bid | -| `place_ask_locks_base_in_vault` | Fund lock on ask | -| `settle_funds_moves_unsettled_base_to_user` | Vault → user ATA transfer via market PDA signer | +- `initialize_market_sets_market_and_order_book`: PDA creation, vault setup, initial field values +- `create_market_user_tracks_market_and_owner`: Per-user PDA derivation and zero-initialised counters +- `place_bid_locks_quote_in_vault`: Fund lock on bid +- `place_ask_locks_base_in_vault`: Fund lock on ask +- `settle_funds_moves_unsettled_base_to_user`: Vault → user ATA transfer via market PDA signer **Validation:** -| Test | Exercises | -|---|---| -| `place_order_rejects_zero_price` | `price > 0` | -| `place_order_rejects_unaligned_tick` | `price % tick_size == 0` | -| `place_order_rejects_below_min_order_size` | `quantity >= min_order_size` | -| `cancel_order_rejects_non_owner` | Ownership check on cancel | -| `initialize_market_rejects_zero_tick_size` | Init constraint | -| `initialize_market_rejects_oversized_fee` | `fee_bps <= 10_000` | +- `place_order_rejects_zero_price`: `price > 0` +- `place_order_rejects_unaligned_tick`: `price % tick_size == 0` +- `place_order_rejects_below_min_order_size`: `quantity >= min_order_size` +- `cancel_order_rejects_non_owner`: Ownership check on cancel +- `initialize_market_rejects_zero_tick_size`: Init constraint +- `initialize_market_rejects_oversized_fee`: `fee_bps <= 10_000` **Cancel + settle flow:** -| Test | Exercises | -|---|---| -| `cancel_ask_credits_unsettled_base` | Ask cancel → `unsettled_base += remaining` | -| `cancel_and_settle_bid_refunds_full_quote` | Round trip of a Bob-style cancellation | +- `cancel_ask_credits_unsettled_base`: Ask cancel → `unsettled_base += remaining` +- `cancel_and_settle_bid_refunds_full_quote`: Round trip of a Bob-style cancellation **Matching engine:** -| Test | Exercises | -|---|---| -| `taker_bid_fully_crosses_best_ask` | Full-fill crossing, fee routed correctly | -| `taker_ask_fully_crosses_best_bid` | Symmetric path | -| `taker_partially_fills_resting_order_rest_stays_on_book` | Resting order's `filled_quantity` updated, not removed | -| `taker_partially_filled_remainder_rests_on_book` | Taker's remainder inserted in correct price order | -| `taker_crosses_multiple_resting_orders_best_price_first` | Walks multiple makers in price priority | -| `resting_orders_at_same_price_fill_by_time_priority` | Tie-break at same price is first-in-first-out | -| `taker_bid_gets_price_improvement_from_resting_ask` | Rebate → `unsettled_quote` | -| `fee_vault_receives_exactly_bps_of_taker_gross` | Fee math in a single batched CPI | -| `authority_can_withdraw_fees_after_match` | Fee drain after fills, authority-gated | -| `settle_funds_after_match_pays_out_both_unsettled_balances` | Both legs paid in one call | +- `taker_bid_fully_crosses_best_ask`: Full-fill crossing, fee routed correctly +- `taker_ask_fully_crosses_best_bid`: Symmetric path +- `taker_partially_fills_resting_order_rest_stays_on_book`: Resting order's `filled_quantity` updated, not removed +- `taker_partially_filled_remainder_rests_on_book`: Taker's remainder inserted in correct price order +- `taker_crosses_multiple_resting_orders_best_price_first`: Walks multiple makers in price priority +- `resting_orders_at_same_price_fill_by_time_priority`: Tie-break at same price is first-in-first-out +- `taker_bid_gets_price_improvement_from_resting_ask`: Rebate → `unsettled_quote` +- `fee_vault_receives_exactly_bps_of_taker_gross`: Fee math in a single batched CPI +- `authority_can_withdraw_fees_after_match`: Fee drain after fills, authority-gated +- `settle_funds_after_match_pays_out_both_unsettled_balances`: Both legs paid in one call ### CI note From 8fc85e10365cc8f086ea11be7ab3de9dfc5e1a4c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 14:37:57 +0000 Subject: [PATCH 4/5] docs(finance): convert tables to prose in order-book/quasar README Applies the same table->prose and em-dash cleanup to the Quasar CLOB port's README (two tables, prose em-dashes, and an 'on-chain' -> 'onchain' fix), so the finance docs are consistent. --- finance/order-book/quasar/README.md | 46 +++++++++++++---------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/finance/order-book/quasar/README.md b/finance/order-book/quasar/README.md index 2f4857cb..445c5a34 100644 --- a/finance/order-book/quasar/README.md +++ b/finance/order-book/quasar/README.md @@ -1,6 +1,6 @@ -# Order Book — Central Limit Order Book (CLOB), Quasar port +# Order Book: Central Limit Order Book (CLOB), Quasar port -A [central limit order book (CLOB)](https://www.investopedia.com/terms/l/limitorderbook.asp) — the market +A [central limit order book (CLOB)](https://www.investopedia.com/terms/l/limitorderbook.asp), the market structure NYSE, NASDAQ, CME, and onchain venues like Phoenix and OpenBook run on. Users post buy or sell offers at prices they pick; the program matches crossing offers in **price-time priority** and settles the resulting token movements. @@ -14,8 +14,8 @@ focuses on how the program works and what the Quasar port does differently. ## What the program does -Two users want to swap tokens at prices they each chose. **Alice** holds USDC (the *quote* mint — the pricing -unit) and wants to buy NVDAx (the *base* mint — the asset being priced), but only at 900 USDC/share or lower. +Two users want to swap tokens at prices they each chose. **Alice** holds USDC (the *quote* mint, the pricing +unit) and wants to buy NVDAx (the *base* mint, the asset being priced), but only at 900 USDC/share or lower. **Bob** holds NVDAx and will sell at 900 or higher. They post a **bid** and an **ask**; when the prices cross, the program fills them. @@ -27,23 +27,21 @@ versus their own limit. The taker pays a fee (in basis points) routed to a fee v credited net of that fee. Funds are held in **program-owned vaults** (the market PDA is their token authority) from the moment an order -locks them until settlement. There is no admin escape hatch — the market authority can only withdraw +locks them until settlement. There is no admin escape hatch: the market authority can only withdraw accumulated **fees**, never user balances. The deployed program bytecode is the only thing that can move vault funds, and it moves them only along the place / cancel / settle paths below. ## Accounts and PDAs -| Account | Kind | Seeds | Role | -| --- | --- | --- | --- | -| `Market` | PDA | `["market", base_mint, quote_mint]` | One trading pair. Stores config + vault addresses. Its PDA is the vaults' token authority. | -| `OrderBook` | keypair account | — (not a PDA) | Two critbit slabs (bids + asks), ~180 KB. Zero-copy. Bound to its market by the market's stored `order_book`. | -| `MarketUser` | PDA | `["market_user", market, owner]` | Per-user, per-market. Tracks open order ids and `unsettled_*` balances owed back to the user. | -| `Order` | PDA | `["order", market, order_id]` | One order. `order_id` is the book's monotonic counter at placement time. | -| `base_vault` / `quote_vault` | token accounts | — | Hold locked funds while orders are open. Market PDA is the authority. | -| `fee_vault` | token account | — | Accumulates taker fees (quote mint). Kept separate so user balances and fees can't be confused. | +- `Market` (PDA, seeds `["market", base_mint, quote_mint]`): One trading pair. Stores config + vault addresses. Its PDA is the vaults' token authority. +- `OrderBook` (keypair account, not a PDA): Two critbit slabs (bids + asks), ~180 KB. Zero-copy. Bound to its market by the market's stored `order_book`. +- `MarketUser` (PDA, seeds `["market_user", market, owner]`): Per-user, per-market. Tracks open order ids and `unsettled_*` balances owed back to the user. +- `Order` (PDA, seeds `["order", market, order_id]`): One order. `order_id` is the book's monotonic counter at placement time. +- `base_vault` / `quote_vault` (token accounts): Hold locked funds while orders are open. Market PDA is the authority. +- `fee_vault` (token account): Accumulates taker fees (quote mint). Kept separate so user balances and fees can't be confused. The order book is **not** a PDA. Solana caps inner-CPI account allocations at 10 KB, so a ~180 KB account can't -be created with an `init` constraint — the client calls `system_program::create_account` directly (sizing it to +be created with an `init` constraint: the client calls `system_program::create_account` directly (sizing it to `ORDER_BOOK_ACCOUNT_SIZE`, program-owned, zeroed) and passes it to `initialize_market`, which verifies and initializes it in place. @@ -63,21 +61,19 @@ NVDAx (9 decimals) / USDC (6 decimals): `base_lot_size = 1000`, `quote_lot_size ## Instruction lifecycle -| Handler | What it does | -| --- | --- | -| `initialize_market` | Create the `Market` PDA, the two vaults, and the fee vault; initialize the pre-created order-book account. | -| `create_market_user` | Create a caller's `MarketUser` for a market. | -| `place_order` | Lock funds, cross the opposing side in price-time priority, credit fills to maker/taker `unsettled_*`, route the taker fee, and rest any remainder. | -| `cancel_order` | Credit an open order's locked remainder back to the owner's `unsettled_*` and remove it from the book. | -| `settle_funds` | Move a user's `unsettled_*` balances out of the vaults into their token accounts. | -| `withdraw_fees` | Authority-only: drain the fee vault to the authority's token account. | +- `initialize_market`: Create the `Market` PDA, the two vaults, and the fee vault; initialize the pre-created order-book account. +- `create_market_user`: Create a caller's `MarketUser` for a market. +- `place_order`: Lock funds, cross the opposing side in price-time priority, credit fills to maker/taker `unsettled_*`, route the taker fee, and rest any remainder. +- `cancel_order`: Credit an open order's locked remainder back to the owner's `unsettled_*` and remove it from the book. +- `settle_funds`: Move a user's `unsettled_*` balances out of the vaults into their token accounts. +- `withdraw_fees`: Authority-only: drain the fee vault to the authority's token account. `place_order` takes `side` (`0` = bid, `1` = ask), `price`, `quantity`, and `order_id`. The caller passes the resting maker orders to cross as **remaining accounts**, in pairs of `(maker_order, maker_market_user)`, in the book's price-time priority. `order_id` must equal the book's current `next_order_id` (the program verifies it), so the client derives the `Order` PDA deterministically. -Fills never transfer tokens directly to the counterparty — they credit `unsettled_*` balances that each user +Fills never transfer tokens directly to the counterparty; they credit `unsettled_*` balances that each user drains later via `settle_funds`. This keeps the per-fill account footprint small (no maker ATAs in the fill path), as in OpenBook v2. @@ -132,8 +128,8 @@ cargo test # QuasarSVM integration tests (they load the compiled .so) `quasar build` must run before `cargo test`: the tests load the compiled `.so` into [QuasarSVM](https://github.com/blueshift-gg/quasar-svm), an in-process SVM, via `include`/`fs::read`. The suite -in `src/tests.rs` drives the full lifecycle — initialize a market, create users, rest an ask, cross it with a -bid, settle both sides, and withdraw the fee — asserting on-chain state, token balances, and fee accounting at +in `src/tests.rs` drives the full lifecycle (initialize a market, create users, rest an ask, cross it with a +bid, settle both sides, and withdraw the fee), asserting onchain state, token balances, and fee accounting at each step, plus an authorization rejection. ## Extending From f0fea13b6ad7b6fa1dd65139d7e90e80f02d0432 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 14:48:48 +0000 Subject: [PATCH 5/5] docs(betting-market): fix awkward phrasing of the trust caveat 'Outcome resolution is still trusted' is not idiomatic - you trust the admin, not the resolution. Reword to 'Resolution still requires trusting the admin, who chooses the winning outcome'. --- finance/betting-market/anchor/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/finance/betting-market/anchor/README.md b/finance/betting-market/anchor/README.md index f914d282..d85417af 100644 --- a/finance/betting-market/anchor/README.md +++ b/finance/betting-market/anchor/README.md @@ -11,8 +11,8 @@ where odds are set by the crowd's stakes rather than by an order book or a fixed ## Purpose It solves the core custody problem of pooled betting: collecting stakes from many bettors, holding -them in one place no single bettor controls, and paying winners by a fixed, public formula. (Outcome -resolution is still trusted: the admin chooses the winning outcome, as described below.) The pool is +them in one place no single bettor controls, and paying winners by a fixed, public formula. Resolution +still requires trusting the admin, who chooses the winning outcome, as described below. The pool is a token account owned by the event's PDA, so payouts are signed by the program with the event's seeds - there is no admin key that can move bettors' stakes out of the pool. The admin's only powers are creating events/outcomes and choosing the winning outcome (or cancelling).