Skip to content

Commit 137130b

Browse files
committed
feat(finance/perpetual-futures): add perpetual-futures example (Anchor + Quasar)
Adds a perpetual-futures exchange example under finance/perpetual-futures/, modelled on the oracle-priced, pool-collateralized design used by Jupiter Perpetuals and GMX (and the open-source solana-labs/perpetuals reference that Adrena and Flash Trade fork). Liquidity providers fund a shared pool and are the counterparty to every trade; traders open leveraged long or short positions priced at a Switchboard oracle, pay open/close and funding fees, and are liquidated permissionlessly once their equity falls below the maintenance margin. Both framework implementations expose the same seven instruction handlers — initialize_pool, add_liquidity, remove_liquidity, open_position, close_position, liquidate_position, collect_fees — and share the same design: - Liquidity-provider shares priced against mark-to-market assets-under-management derived from per-side open-interest accumulators, so an exiting provider cannot dodge an in-flight trader profit. - Reserved-liquidity solvency model: each open position reserves its notional size, an open is allowed only while the reserve stays backed by liquidity (which also caps open interest), recoverable profit is capped at the reserve so a winner can always be paid, and liquidity-provider withdrawals can take only the unreserved remainder. - A cumulative funding index in which the heavier open-interest side pays the pool over time. - Oracle reads validated for staleness (by slot), positivity, scale, and a confidence band that must stay within a per-pool maximum. - All money math in u128 with checked arithmetic, multiply-before-divide, rounding toward the protocol; transfer_checked; checks-effects-interactions; slippage bounds on every state-changing handler. Anchor (finance/perpetual-futures/anchor): a perpetual-futures program plus a mock-switchboard companion program that supplies a deterministic price+confidence feed for tests and documents the swap-in point for a real Switchboard On-Demand feed. Covered by 22 LiteSVM integration tests. Quasar (finance/perpetual-futures/quasar): a port with 12 quasar-svm tests. It keeps one position per (pool, owner) because Quasar's address constraint can reference account inputs but not instruction arguments, so the side is stored in the position rather than used as a seed; the oracle feed is supplied directly in tests rather than via a companion program. Docs: a finance-oriented README following the repository's program-flow format (participants, sample USDC/NVDAx tokens, per-step instruction and account tables) with concept links, a terminology glossary, design notes relating the example to percolator and solana-labs/perpetuals, and a Financial Software entry in the root README. Also commits the QuickNode Solana coding skill under .claude/skills/ (with a tracked .gitignore exception) so it is available in Claude Code web sessions. https://claude.ai/code/session_01YNkQe8eneL4FxCu96kmjVr
1 parent 403f003 commit 137130b

48 files changed

Lines changed: 6153 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Quiknode Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Rust Guidelines (Anchor Programs)
2+
3+
These guidelines apply to Anchor programs and any Rust crates that use Solana dependencies. Read this alongside the general rules in [SKILL.md](SKILL.md).
4+
5+
## Anchor Version
6+
7+
- Write all code like the latest stable Anchor (currently 1.0.2 but there may be a newer version by the time you read this)
8+
- Use LiteSVM and Rust tests for new Anchor programs. `anchor init` uses LiteSVM by default.
9+
- Do not use unnecessary macros that are not needed in the latest stable Anchor
10+
- Don't implement instruction handlers as methods on account structs. There's no reason to tie state to functions, the function is not modifying the state (if we did like OOP, which we don't), and the functions and structs work without doing this, so there's no reason to implement instruction handlers as methods on account structs.
11+
12+
## Anchor has silly defaults
13+
14+
Every project will need an IDL.
15+
16+
```toml
17+
[features]
18+
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
19+
```
20+
21+
and if it uses Tokens (like almost every Anchor project) it will need this dependency (insert whatever version is applicable):
22+
23+
```toml
24+
[dependencies]
25+
anchor-spl = "1.0.2"
26+
```
27+
28+
## Project Structure
29+
30+
- **Never modify the program ID** in `lib.rs` or `Anchor.toml` when making changes
31+
- Create files inside the `state` folder for whatever state is needed
32+
- Create files inside the `instructions` or `handlers` folders (whichever exists) for whatever instruction handlers are needed
33+
- Put Account Constraints in instruction files, but ensure the names end with `AccountConstraints` rather than just naming them the same thing as the function
34+
- Handlers that are only for the admin should be in a new folder called `admin` inside whichever parent folder exists (`instructions/admin/` or `handlers/admin/`)
35+
36+
## Account Constraints
37+
38+
- Use a newline after each key in the account constraints struct, so the macro and the matching key/value have some space from other macros and their matching key/value
39+
40+
## Bumps
41+
42+
- Use `context.bumps.foo` not `context.bumps.get("foo").unwrap()` - the latter is outdated
43+
44+
## Data Structures
45+
46+
- When making structs ensure strings and Vectors have a `max_len` attribute
47+
- Vectors have two numbers for `max_len`: the first is the max length of the vector, the second is the max length of the items in the vector
48+
49+
## Space Calculation (CRITICAL - NO MAGIC NUMBERS)
50+
51+
- **Do not use magic numbers anywhere**. I don't want to see `8 + 32` or whatever.
52+
- **Do not make constants for the sizes of various data structures**
53+
- For `space`, use syntax like: `space = SomeStruct::DISCRIMINATOR.len() + SomeStruct::INIT_SPACE,`
54+
- All structs should have `#[derive(InitSpace)]` added to them, to get the `INIT_SPACE` trait
55+
- **DO NOT use magic numbers**
56+
57+
**Example:**
58+
59+
```rust
60+
#[derive(InitSpace)]
61+
#[account]
62+
pub struct UserProfile {
63+
pub authority: Pubkey,
64+
65+
#[max_len(50)]
66+
pub username: String,
67+
68+
pub bump: u8,
69+
}
70+
71+
#[derive(Accounts)]
72+
pub struct InitializeProfile<'info> {
73+
#[account(
74+
init,
75+
payer = authority,
76+
space = UserProfile::DISCRIMINATOR.len() + UserProfile::INIT_SPACE,
77+
seeds = [b"profile", authority.key().as_ref()],
78+
bump
79+
)]
80+
pub profile: Account<'info, UserProfile>,
81+
82+
#[account(mut)]
83+
pub authority: Signer<'info>,
84+
85+
pub system_program: Program<'info, System>,
86+
}
87+
```
88+
89+
## Error Handling
90+
91+
- Return useful error messages
92+
- Write code to handle common errors like insufficient funds, bad values for parameters, and other obvious situations
93+
- All arithmetic in onchain code is `checked_*` — never raw `+ - * /`. Solana's BPF doesn't trap on overflow in release builds; silent wraps are how hacks happen. `checked_*` returns `Option`; force the error with `.ok_or(MyError::MathOverflow)?`. Reserve `saturating_*` for cosmetic/UX display values, never for balances.
94+
95+
## Onchain Financial Math
96+
97+
Applies to any code touching money, balances, prices, shares, fees, or token amounts. These rules are non-negotiable.
98+
99+
- **Integers only — no floats, no fixed-point libraries.** Floats are non-deterministic across platforms (different validators could disagree on state). `fixed::types::I64F64`, `rust_decimal`, `bnum`-fixed-point and similar are also out — they add audit surface, burn compute, and hide the rounding/precision decisions you should be making explicitly. Token amounts are integers (base units), prices are ratios of integers. The system is discrete. Production Solana AMMs (Orca, Raydium, Meteora, Saber, Phoenix) all use raw `u128`. If you find yourself reaching for a decimal type, stop — the right tool is `u128` with discipline.
100+
- **Multiply before you divide.** `a * b / c`, not `(a / c) * b`. Division truncates; dividing first throws away precision permanently.
101+
- **Use `u128` (or wider) for intermediate products.** `u64 * u64` overflows at ~1.8e19. Cast both operands to `u128` _before_ multiplying, then narrow the final result with `try_into().map_err(|_| MyError::MathOverflow)?`.
102+
- **Round in the protocol's favour, never the user's.** Value-to-share and share-to-value conversions: user gets floor, protocol gets ceil. Otherwise you leak 1 base unit per transaction forever, and attackers will industrialise it.
103+
- **Validate ranges before doing the math.** Reject zero inputs, `amount > balance`, ratios that would mint zero shares. Cheap, prevents the inflation/donation attack on empty pools and other whole bug classes.
104+
- **Check invariants after the math, not just before.** "K must not decrease" on a swap, "total LP shares == sum of holdings", "reserves >= owed fees". Compute, then `require!()` the invariant.
105+
- **Decimals are tracked, not assumed.** USDC=6, SOL=9, SPL tokens vary. Use `transfer_checked` (carries decimals in the CPI). Reserves hold raw base units; the UI does cosmetic conversion. Never hard-code `* 10^9`.
106+
- **Oracle/price freshness is part of the math.** Check `last_updated_slot` and reject if older than N slots. A stale price means the calculation is wrong.
107+
- **Oracle confidence is part of the math too.** Pull oracles (Pyth, Switchboard) report a price *and* a confidence/uncertainty band. Reject the update when the band is too wide relative to the price (e.g. `confidence * 10_000 / price > max_error_bps`); a wide band means the price is unreliable, and skipping this check is one of the most common oracle exploits. Where the feed offers it, prefer the EMA/TWAP price over the latest spot price for a mark that is harder to manipulate within a single block. (See `solana-labs/perpetuals` for a worked example.)
108+
- **Checks-effects-interactions.** Update state before the token transfer CPI, not after.
109+
- **Treat client-supplied values as adversarial.** If a handler takes `(amount_a, amount_b)`, verify each against onchain state, not against each other.
110+
- **Test the branch the bug lives in.** Standard AMM/lending bugs sit in the _non-empty pool_, _post-swap_, _post-fee_, _rounding-edge_ branches. The happy path almost always works. Write the test that exercises the branch where the bug actually lives.
111+
- **LP shares use different formulas for first deposit vs subsequent.** First deposit: shares = `sqrt(amount_a * amount_b)` (geometric mean bootstraps the pool). Subsequent deposits: shares = `min(amount_a * supply / reserve_a, amount_b * supply / reserve_b)` (proportional to share-of-pool). Using the geometric mean for every deposit is a real, repeated bug — test both branches separately.
112+
- **For integer sqrt, hand-code Newton's method on `u128`** (~15 lines, as Uniswap V2 in Solidity / Saber in Rust do). Don't reach for a fixed-point crate for one sqrt.
113+
- **Slippage protection: accept a `min_output_*` from the user and verify before the CPI.** Swaps, deposits, and withdraws all need it. Without it, sandwich attackers steal value across the price gap they create.
114+
- **Never silently clamp user input to balance.** If a user asks to swap 100 and you clamp to 80 because that's the balance, the user's slippage check passes against the wrong amount. Either fail the instruction or return the actual amount so the client can validate.
115+
- **Use `transfer_checked`, never raw `transfer`.** `transfer_checked` carries the mint and decimals through the CPI, so a wrong-mint or wrong-decimals account causes a CPI failure instead of a silent miscalculation.
116+
- **For token program compatibility, use `anchor_spl::token_interface`** (`InterfaceAccount<TokenAccount>`, `InterfaceAccount<Mint>`, `Interface<TokenInterface>`). The same code then works against both the Classic Token Program and the Token Extensions Program.
117+
- **Oracle freshness uses slots, not unix time.** Slot count is what the runtime guarantees; `Clock::get()?.unix_timestamp` is validator-influenced. Check `last_updated_slot` against `Clock::get()?.slot` and reject if older than N slots. If you must use a unix timestamp (because the oracle only exposes one), state why in a comment.
118+
- **Canonical pubkey ordering for two-asset pools.** Order mints so `mint_a.key() < mint_b.key()` (lexicographic on the 32-byte key). Same pool whether the user passes `(USDC, SOL)` or `(SOL, USDC)`. Enforce in the constraint, don't rely on the client.
119+
120+
### Escrows, Vaults, and Escape Hatches
121+
122+
- **Every escrow needs a cancel/withdraw instruction.** An escrow with no cancel locks abandoned offers forever — funds become unrecoverable when the counterparty disappears. The cancel must be callable by the maker (and only the maker) at any time before the trade settles.
123+
- **Don't use `init_if_needed` for an account the wrong party would pay rent for.** Common bug: the taker's instruction lazily creates the maker's destination ATA via `init_if_needed`, so the taker pays the maker's rent. Either require the maker to pre-create their ATA or pass the rent payer explicitly.
124+
- **Update state before the CPI.** Already in the list above, but worth repeating in the vault context: write the new balance/share count first, then transfer. A CPI that re-enters (rare on Solana but possible via callbacks) sees current state, not stale state.
125+
126+
**Pattern to copy when ratio-clamping (Uniswap V2 style):**
127+
128+
```rust
129+
let pool_a = pool_a_amount as u128;
130+
let pool_b = pool_b_amount as u128;
131+
let amount_a_u128 = amount_a as u128;
132+
let amount_b_u128 = amount_b as u128;
133+
134+
// Multiply before divide; u128 prevents overflow.
135+
let amount_b_required = amount_a_u128
136+
.checked_mul(pool_b).ok_or(ErrorCode::MathOverflow)?
137+
.checked_div(pool_a).ok_or(ErrorCode::MathOverflow)?;
138+
139+
let (final_a, final_b) = if amount_b_required <= amount_b_u128 {
140+
(amount_a_u128, amount_b_required)
141+
} else {
142+
let amount_a_required = amount_b_u128
143+
.checked_mul(pool_a).ok_or(ErrorCode::MathOverflow)?
144+
.checked_div(pool_b).ok_or(ErrorCode::MathOverflow)?;
145+
(amount_a_required, amount_b_u128)
146+
};
147+
148+
let final_a: u64 = final_a.try_into().map_err(|_| ErrorCode::MathOverflow)?;
149+
let final_b: u64 = final_b.try_into().map_err(|_| ErrorCode::MathOverflow)?;
150+
```
151+
152+
## Cargo hygiene
153+
154+
- Run `cargo clean` after finishing with a Rust project. Anchor `target/` directories accumulate fast (multi-GiB per project).
155+
- If disk usage hits 85%, clean before doing more work.
156+
157+
## PDA Management
158+
159+
- Add `pub bump: u8` to every struct stored in PDA
160+
- Save the bumps inside each when the struct inside the PDA is created
161+
162+
## System Functions
163+
164+
- When you get the time via Clock, use `Clock::get()?;` rather than `anchor_lang::solana_program::clock`

0 commit comments

Comments
 (0)