From 7a226f60c4ca80b43dc94efb562d72973c09ab6e Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:45:46 +0900 Subject: [PATCH 01/20] Look up the buffers of the most traded mints at compile time `validate_buffer_pda` now checks a compile-time table of the buffer PDAs for the 63 most traded verified tokens before falling back to `create_program_address`, saving ~1430 CU per push of a known buy mint at the cost of ~76 CU per push of any other. `just generate-known-mints` refreshes the mint list from Jupiter's verified token list, ranked by 24h volume. The buffer addresses are derived from it with const-crypto, like `STATE_PDA`. Co-Authored-By: Claude Opus 5.5 (1M context) --- Justfile | 5 + bench-report.json | 25 +-- interface/scripts/generate-known-mints.mjs | 45 +++++ interface/src/pda/buffer.rs | 155 +++++++++++++++++- interface/src/pda/buffer/known_mints.rs | 69 ++++++++ programs/settlement/tests/common/token.rs | 15 ++ .../tests/finalize_settle_pushes.rs | 30 +++- 7 files changed, 330 insertions(+), 14 deletions(-) create mode 100644 interface/scripts/generate-known-mints.mjs create mode 100644 interface/src/pda/buffer/known_mints.rs diff --git a/Justfile b/Justfile index fa3049d7..2d6dde05 100644 --- a/Justfile +++ b/Justfile @@ -31,6 +31,11 @@ build: build-program # Runs all the generated code jobs generate: generate-js-client +# Refresh the most traded token mints whose buffer PDAs the settlement program +# derives at compile time. Not part of `generate`: it snapshots live market data. +generate-known-mints: + node interface/scripts/generate-known-mints.mjs + # Builds the JS/TS client from IDL. [working-directory: 'programs/settlement/idl'] @generate-js-client: diff --git a/bench-report.json b/bench-report.json index cdfd58ab..319fcc27 100644 --- a/bench-report.json +++ b/bench-report.json @@ -35,6 +35,7 @@ "settle/pulls_funds_to_destination": 11, "settle/pulls_to_multiple_destinations": 12, "settle/pushes_a_single_order": 10, + "settle/pushes_a_single_order_of_a_known_mint": 10, "settle/pushes_several_orders_from_different_buffers": 14, "settle/pushes_several_orders_from_one_buffer": 13, "settle/settles_a_single_order": 10, @@ -76,17 +77,18 @@ "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null, "remove_solver/remove_with_many_existing_solvers": 754, "remove_solver/removes_a_solver": 489, - "settle/finalizes_with_no_pushes": 1106, - "settle/happy_path_sell_tokens_for_native_sol": 4613, - "settle/pulls_from_multiple_orders": 13282, - "settle/pulls_funds_to_destination": 7229, - "settle/pulls_to_multiple_destinations": 8382, - "settle/pushes_a_single_order": 6067, - "settle/pushes_several_orders_from_different_buffers": 10957, - "settle/pushes_several_orders_from_one_buffer": 10957, - "settle/settles_a_single_order": 6085, - "settle/settles_multiple_orders": 15900, - "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7222, + "settle/finalizes_with_no_pushes": 1108, + "settle/happy_path_sell_tokens_for_native_sol": 4619, + "settle/pulls_from_multiple_orders": 13432, + "settle/pulls_funds_to_destination": 7305, + "settle/pulls_to_multiple_destinations": 8458, + "settle/pushes_a_single_order": 6143, + "settle/pushes_a_single_order_of_a_known_mint": 4636, + "settle/pushes_several_orders_from_different_buffers": 11108, + "settle/pushes_several_orders_from_one_buffer": 11107, + "settle/settles_a_single_order": 6161, + "settle/settles_multiple_orders": 16126, + "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7299, "transfer_authority/manager_can_transfer_manager": 158, "transfer_authority/manager_can_transfer_reclaim_authority": 162, "transfer_authority/manager_can_transfer_settlement_owned_order_authority": 161, @@ -129,6 +131,7 @@ "settle/pulls_funds_to_destination": 507, "settle/pulls_to_multiple_destinations": 548, "settle/pushes_a_single_order": 466, + "settle/pushes_a_single_order_of_a_known_mint": 466, "settle/pushes_several_orders_from_different_buffers": 608, "settle/pushes_several_orders_from_one_buffer": 576, "settle/settles_a_single_order": 466, diff --git a/interface/scripts/generate-known-mints.mjs b/interface/scripts/generate-known-mints.mjs new file mode 100644 index 00000000..8456c43a --- /dev/null +++ b/interface/scripts/generate-known-mints.mjs @@ -0,0 +1,45 @@ +// Writes `src/pda/buffer/known_mints.rs`: the mints of the most traded verified +// Solana tokens, whose buffer PDAs the interface derives at compile time so the +// settlement program can skip re-deriving them on-chain. +// +// Ranks Jupiter's verified token list by 24h volume. Jupiter's own `toptraded` +// ranking isn't used because it leaves out base tokens such as SOL and USDC. + +import { writeFileSync } from "node:fs"; + +const COUNT = 63; +const SOURCE = "https://lite-api.jup.ag/tokens/v2/tag?query=verified"; +const OUTPUT = new URL("../src/pda/buffer/known_mints.rs", import.meta.url); + +const response = await fetch(SOURCE); +if (!response.ok) { + throw new Error(`${SOURCE} answered ${response.status}`); +} +const tokens = await response.json(); + +const volume = ({ stats24h }) => + (stats24h?.buyVolume ?? 0) + (stats24h?.sellVolume ?? 0); +const ranked = tokens.sort((a, b) => volume(b) - volume(a)).slice(0, COUNT); +if (ranked.length !== COUNT) { + throw new Error( + `expected at least ${COUNT} verified tokens, got ${ranked.length}`, + ); +} + +// Symbols are free-form text; keep only what reads safely in a line comment. +const label = (symbol) => symbol.replace(/[^\x20-\x7e]/g, "").trim() || "?"; +const date = new Date().toISOString().slice(0, 10); +// Aligns the symbol comments the way rustfmt does. +const width = Math.max(...ranked.map(({ id }) => id.length)); + +writeFileSync( + OUTPUT, + `// @generated by \`just generate-known-mints\` on ${date}. Do not edit by hand. + +/// The ${COUNT} verified tokens with the highest 24h trading volume on Jupiter, +/// most traded first. +pub(super) const KNOWN_MINTS: [&str; ${COUNT}] = [ +${ranked.map(({ id, symbol }) => ` ${`"${id}",`.padEnd(width + 3)} // ${label(symbol)}`).join("\n")} +]; +`, +); diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index 9302e57b..45fb2020 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -26,6 +26,8 @@ use solana_pubkey::Pubkey; use crate::pda::{is_pda_with_signer_seeds, SETTLEMENT_SEED}; use crate::SettlementError; +mod known_mints; + /// Trailing seed identifying the buffer PDAs. pub const BUFFER_SEED: &[u8] = b"buffer"; @@ -35,7 +37,7 @@ pub const BUFFER_SEED: &[u8] = b"buffer"; /// `mint` is the raw 32-byte token mint address, so the same helper serves /// both the off-chain builder and the on-chain handler (which holds the mint /// as an `Address`). -pub fn buffer_pda_seeds(mint: &[u8; 32]) -> [&[u8]; 3] { +pub const fn buffer_pda_seeds(mint: &[u8; 32]) -> [&[u8]; 3] { [SETTLEMENT_SEED, mint, BUFFER_SEED] } @@ -50,8 +52,82 @@ pub fn find_buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&buffer_pda_seeds(mint.as_array()), program_id) } +/// A buffer PDA under [`crate::ID`] derived at compile time. +struct KnownBuffer { + mint: [u8; 32], + address: [u8; 32], +} + +/// The buffers of [`known_mints::KNOWN_MINTS`], ordered by [`search_key`] so +/// the program can binary search [`KNOWN_BUFFER_KEYS`] instead of paying for a +/// `create_program_address` syscall. +// Deriving this many PDAs in const eval trips the compiler's infinite-loop +// guard, although it finishes in seconds. +#[allow(long_running_const_eval)] +const KNOWN_BUFFERS: [KnownBuffer; known_mints::KNOWN_MINTS.len()] = { + let mut buffers = [const { + KnownBuffer { + mint: [0; 32], + address: [0; 32], + } + }; known_mints::KNOWN_MINTS.len()]; + let mut i = 0; + while i < buffers.len() { + let mint = const_crypto::bs58::decode_pubkey(known_mints::KNOWN_MINTS[i]); + let (address, _) = const_crypto::ed25519::derive_program_address( + &buffer_pda_seeds(&mint), + crate::ID.as_array(), + ); + // Insertion sort: shift every buffer with a larger key up one slot. + let mut j = i; + while j > 0 && search_key(&buffers[j - 1].mint) > search_key(&mint) { + buffers.swap(j - 1, j); + j -= 1; + } + buffers[j] = KnownBuffer { mint, address }; + i += 1; + } + buffers +}; + +/// The [`search_key`] of each of [`KNOWN_BUFFERS`], in the same order. +const KNOWN_BUFFER_KEYS: [u64; KNOWN_BUFFERS.len()] = { + let mut keys = [0; KNOWN_BUFFERS.len()]; + let mut i = 0; + while i < keys.len() { + keys[i] = search_key(&KNOWN_BUFFERS[i].mint); + // Strictly increasing, so a key identifies at most one known buffer. + assert!( + i == 0 || keys[i - 1] < keys[i], + "known mints must have distinct search keys" + ); + i += 1; + } + keys +}; + +/// The leading 8 bytes of `mint`: comparing a `u64` is cheaper on-chain than +/// comparing the whole address. +const fn search_key(mint: &[u8; 32]) -> u64 { + u64::from_le_bytes(*mint.first_chunk().expect("a mint is longer than 8 bytes")) +} + +/// The compile-time buffer for `mint`, if it's one of the known mints and +/// `program_id` is the one the table was derived under. +fn known_buffer(program_id: &Address, mint: &[u8; 32]) -> Option<&'static KnownBuffer> { + if program_id != &crate::ID { + return None; + } + let index = KNOWN_BUFFER_KEYS.binary_search(&search_key(mint)).ok()?; + KNOWN_BUFFERS.get(index).filter(|known| &known.mint == mint) +} + /// Confirm `buffer` matches the derived buffer PDA for the mint bytes `mint` /// and `bump`. +/// +/// A known mint is checked against its compile-time buffer alone, ignoring +/// `bump`: nothing signs with a buffer's seeds, so the bump only matters for +/// re-deriving the address. #[inline] #[must_use = "ignoring the output means ignoring the validation result"] pub fn validate_buffer_pda( @@ -60,7 +136,13 @@ pub fn validate_buffer_pda( mint: &[u8; 32], bump: u8, ) -> Result<(), ProgramError> { - is_pda_with_signer_seeds(buffer, program_id, buffer_pda_signer_seeds(mint, &[bump])) + let is_buffer = match known_buffer(program_id, mint) { + Some(known) => buffer.as_array() == &known.address, + None => { + is_pda_with_signer_seeds(buffer, program_id, buffer_pda_signer_seeds(mint, &[bump])) + } + }; + is_buffer .then_some(()) .ok_or(SettlementError::PushSourceNotBuffer.into()) } @@ -125,6 +207,75 @@ mod tests { assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); } + /// The USDC mint, one of [`known_mints::KNOWN_MINTS`]. + const USDC: Pubkey = Pubkey::from_str_const("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + + #[test] + fn known_buffers_are_canonical() { + assert_eq!(KNOWN_BUFFERS.len(), known_mints::KNOWN_MINTS.len()); + for known in &KNOWN_BUFFERS { + let (pda, _) = find_buffer_pda(&crate::ID, &Pubkey::new_from_array(known.mint)); + assert_eq!(known.address, *pda.as_array()); + } + } + + #[test] + fn every_known_mint_has_a_known_buffer() { + for mint in known_mints::KNOWN_MINTS { + let mint = Pubkey::from_str_const(mint); + assert!( + known_buffer(&crate::ID, mint.as_array()).is_some(), + "{mint}" + ); + } + } + + #[test] + fn unknown_mint_sharing_a_search_key_has_no_known_buffer() { + let mut mint = *USDC.as_array(); + mint[31] ^= 1; + assert!(known_buffer(&crate::ID, &mint).is_none()); + } + + #[test] + fn known_mint_under_another_program_has_no_known_buffer() { + assert!(known_buffer(&Pubkey::new_unique(), USDC.as_array()).is_none()); + } + + #[test] + fn accepts_the_known_buffer() { + let (pda, bump) = find_buffer_pda(&crate::ID, &USDC); + + validate_buffer_pda(&crate::ID, &pda, USDC.as_array(), bump) + .expect("the known buffer PDA must be accepted"); + } + + #[test] + fn rejects_an_invalid_address_for_a_known_mint() { + let (_, bump) = find_buffer_pda(&crate::ID, &USDC); + + let err = validate_buffer_pda(&crate::ID, &Pubkey::new_unique(), USDC.as_array(), bump) + .expect_err("a non-canonical address must be rejected"); + assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); + } + + #[test] + fn ignores_the_bump_for_a_known_mint() { + let (pda, bump) = find_buffer_pda(&crate::ID, &USDC); + + validate_buffer_pda(&crate::ID, &pda, USDC.as_array(), bump ^ 1) + .expect("the known buffer PDA must be accepted whatever the bump"); + } + + #[test] + fn derives_a_known_mint_buffer_under_another_program() { + let program_id = Pubkey::new_unique(); + let (pda, bump) = find_buffer_pda(&program_id, &USDC); + + validate_buffer_pda(&program_id, &pda, USDC.as_array(), bump) + .expect("the buffer PDA under another program must be accepted"); + } + mod proptest { use ::proptest::prelude::*; diff --git a/interface/src/pda/buffer/known_mints.rs b/interface/src/pda/buffer/known_mints.rs new file mode 100644 index 00000000..6aa8753b --- /dev/null +++ b/interface/src/pda/buffer/known_mints.rs @@ -0,0 +1,69 @@ +// @generated by `just generate-known-mints` on 2026-09-30. Do not edit by hand. + +/// The 63 verified tokens with the highest 24h trading volume on Jupiter, +/// most traded first. +pub(super) const KNOWN_MINTS: [&str; 63] = [ + "So11111111111111111111111111111111111111112", // SOL + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC + "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT + "9BEcn9aPEmhSPbPQeFGjidRiEKki46fVQDyPpSQXPA2D", // jlUSDC + "Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh", // NVDAx + "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", // PYUSD + "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", // PUMP + "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij", // cbBTC + "2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH", // USDG + "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", // STONK + "DJTu7vi8norVzdVAffgvb39VP7wjKeTsgaMBJrzfxvoF", // DJT + "A7bdiYdS5GjqGFtxf17ppRHtDKPkkRqbKtR27dxvQXaS", // ZEC + "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", // SPCXx + "oPAiAikWTaFj9RYoRFD35ccfwhnMcB3ThgBZRHSkjTZ", // tOpenAI + "NFLX7qV57zuVxCoHy3s1jiGZyALraLNwttbxdvmYJLJ", // NFLX + "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", // ETH + "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB", // USD1 + "3ZLekZYq2qkZiSpnSvabjit34tUkjSwD1JFuW9as9wBG", // wNEAR + "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", // JUP + "JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD", // JupUSD + "2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv", // PENGU + "98sMhvDwXj1RQi5c5Mndm3vPe9cBqPrbLaufMXFNMh5g", // HYPE + "Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk", // USELESS + "CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH", // CASH + "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", // RAY + "XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W", // SPYx + "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", // TRUMP + "CARDSccUMFKoPRZxt5vt3ksUbxEFEcnZ3H2pd3dKxYjp", // CARDS + "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", // WBTC + "SPCXxcqXj6e5dJDVNovHN8744zkbhM2bYudU45BimGb", // SPCX + "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", // JLP + "BPxxfRCXkUVhig4HS1Lh7kZqV6SPJhzfEk4x6fVBjPCy", // BP + "RKLBnAXGqv31iZomqsuAWkQm1aqC7JwwvbCfzGdqAhz", // RKLB + "Ai66LHZG9MCzg1WKdawwqduVAXpNDUuV8M3uyq5ppump", // CATE + "SKHYhSjuRWHgikq8eRKbtBbpABgJSkd7ytQV14i9EQ3", // SKHY + "Xsa62P5mvPszXL1krVUnU5ar38bBSVcWAB6fmPCo5Zu", // METAx + "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", // JitoSOL + "Grass7B4RdKfBCjTKgSqnXkqjwiGvQyFbuSCUJr3XXjs", // GRASS + "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", // Bonk + "DEkqHyPN7GMRJ5cArtQFAWefqbZb33Hyf6s5iCwjEonT", // USDe + "9cRCn9rGT8V2imeM2BaKs13yhMEais3ruM3rPvTGpump", // ANSEM + "Pren1FvFX6J3E4kXhJuCiAD5aDmGEb7qJRncwA8Lkhw", // ANTHROPIC + "METvsvVRapdj9cFLzq4Tr43xK4tAjQfwX76z3n6mWQL", // MET + "MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5", // MEW + "SNDKbwMUQvZhnLnxLduradgLHG5KrPuKwpnrkkGRhfH", // SNDK + "Xsv9hRk1z5ystj9MhnA7Lq4vjSsLwzL2nxrwmwtD3re", // GLDx + "KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS", // KMNO + "XsueG8BtpquVJX9LVLLEGuViXUungE6WmK5YZ3p3bd1", // CRCLx + "5Y8NV33Vv7WbnLfq3zBcKSdYPrk7g2KoiQoe7M2tcxp5", // ONyc + "4sWNB8zGWHkh6UnmwiEtzNxL4XrN7uK9tosbESbJFfVs", // xSOL + "WXMRyRZhsa19ety5erZhHg4N3xj3EVN92u94422teJp", // XMR + "HcRLc9VDgjLeK154xDawfb1dmVJ98DoSqcwTHGqiDeJR", // ZCAT + "9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump", // Fartcoin + "9fvHrYNw1A8Evpcj7X2yy4k4fT7nNHcA9L6UsamNHAif", // jlUSDG + "CWZ6BsdnjkDVTGkmL6bGbJXXig6ceef12KvyGQW14cMt", // ANTFUN + "5YMkXAYccHSGnHn9nob9xEvv6Pvka9DZWH7nTbotTu9E", // hyUSD + "MUxEsUKSMACyw5fZf68wxf5FLnZVhtU9CwH8uNNGay1", // MU + "PreweJYECqtQwBtpxHL171nL2K6umo692gTm7Q3rpgF", // OPENAI + "XspzcW1PRtgf6Wj92HCiZdjzKCyFekVD8P5Ueh3dRMX", // MSFTx + "HTmQz7My6MehV7bjhJ6jde8nDND1yvsz68d24LP7YgUQ", // GP + "7GxATsNMnaC88vdwd2t3mwrFuQwwGvmYPrUQ4D6FotXk", // JUICED + "HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr", // EURC + "DoGEV7LASBkQbibMc5k5vKnTZoMg423GpJ5QtJEGfm7R", // DOGE +]; diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 7733c524..a200dbd7 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -58,6 +58,21 @@ pub fn create_native_mint(svm: &mut LiteSVM) { ); } +/// Plant a mint at `mint`, an address no test holds the keypair for (such as a +/// real token's), under the legacy SPL Token program with `authority` as its +/// mint authority. +pub fn plant_mint(svm: &mut LiteSVM, mint: Pubkey, authority: &Pubkey) { + let mut data = vec![0u8; Mint::LEN]; + Mint { + mint_authority: Some(*authority).into(), + decimals: 6, + is_initialized: true, + ..Default::default() + } + .pack_into_slice(&mut data); + super::create_account_at(svm, mint, &TokenProgram::SplToken.address(), &data); +} + /// Create a fresh mint under [`active_token::program`], whose mint authority is /// `payer`, and return its address. pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index 7c7f2c4c..25533ff0 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -29,7 +29,7 @@ use cow_settlement_client::{ }; use litesvm_token::spl_token::error::TokenError; use solana_sdk::{ - instruction::InstructionError, program_error::ProgramError, signer::Signer, + instruction::InstructionError, program_error::ProgramError, pubkey::Pubkey, signer::Signer, transaction::TransactionError, }; @@ -80,6 +80,34 @@ fn pushes_a_single_order() { assert_eq!(token::balance(&svm, &buffer_pda), funding - amount); } +#[test] +fn pushes_a_single_order_of_a_known_mint() { + let (mut svm, program_id, payer, solver) = setup_settle_ready(); + // USDC, one of the mints whose buffer the program knows at compile time. + let mint = Pubkey::from_str_const("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + token::plant_mint(&mut svm, mint, &payer.pubkey()); + let intent = OrderBuilder::new(&mut svm, &program_id, &payer) + .buy_mint(&mint) + .build(); + let funding = 1_000; + let buffer_pda = buffer::ensure_funded(&mut svm, &program_id, &payer, &mint, funding); + + let amount = 400; + let instructions = build_matching_settlement( + &program_id, + &solver.pubkey(), + &[FinalizedIntent { + intent: &intent, + amount, + }], + ); + send_metered(&mut svm, &solver, &instructions, BenchLabel::Settle) + .expect("a single push should be paid"); + + assert_eq!(token::balance(&svm, &buy_account(&intent)), amount); + assert_eq!(token::balance(&svm, &buffer_pda), funding - amount); +} + #[test] fn pushes_several_orders_from_one_buffer() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); From 9eb5cd938e748d74e7c0bf611a04f28e1caf5c24 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:03:54 +0900 Subject: [PATCH 02/20] Drop the program ID check from the known buffer lookup The settlement program only works at `crate::ID` (`Initialize` fails elsewhere because of `STATE_PDA`), so checking `program_id` before the known buffer lookup costs 18 CU per push for nothing. Also check that every known mint looks up its own entry and canonical address, not just some entry. Co-Authored-By: Claude Opus 5.5 (1M context) --- bench-report.json | 24 +++++++++++----------- interface/src/pda/buffer.rs | 41 +++++++++++++------------------------ 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/bench-report.json b/bench-report.json index 319fcc27..b80fc18c 100644 --- a/bench-report.json +++ b/bench-report.json @@ -77,18 +77,18 @@ "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null, "remove_solver/remove_with_many_existing_solvers": 754, "remove_solver/removes_a_solver": 489, - "settle/finalizes_with_no_pushes": 1108, - "settle/happy_path_sell_tokens_for_native_sol": 4619, - "settle/pulls_from_multiple_orders": 13432, - "settle/pulls_funds_to_destination": 7305, - "settle/pulls_to_multiple_destinations": 8458, - "settle/pushes_a_single_order": 6143, - "settle/pushes_a_single_order_of_a_known_mint": 4636, - "settle/pushes_several_orders_from_different_buffers": 11108, - "settle/pushes_several_orders_from_one_buffer": 11107, - "settle/settles_a_single_order": 6161, - "settle/settles_multiple_orders": 16126, - "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7299, + "settle/finalizes_with_no_pushes": 1105, + "settle/happy_path_sell_tokens_for_native_sol": 4613, + "settle/pulls_from_multiple_orders": 13396, + "settle/pulls_funds_to_destination": 7286, + "settle/pulls_to_multiple_destinations": 8438, + "settle/pushes_a_single_order": 6125, + "settle/pushes_a_single_order_of_a_known_mint": 4618, + "settle/pushes_several_orders_from_different_buffers": 11074, + "settle/pushes_several_orders_from_one_buffer": 11073, + "settle/settles_a_single_order": 6143, + "settle/settles_multiple_orders": 16076, + "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7280, "transfer_authority/manager_can_transfer_manager": 158, "transfer_authority/manager_can_transfer_reclaim_authority": 162, "transfer_authority/manager_can_transfer_settlement_owned_order_authority": 161, diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index 45fb2020..d75dc5e2 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -112,12 +112,8 @@ const fn search_key(mint: &[u8; 32]) -> u64 { u64::from_le_bytes(*mint.first_chunk().expect("a mint is longer than 8 bytes")) } -/// The compile-time buffer for `mint`, if it's one of the known mints and -/// `program_id` is the one the table was derived under. -fn known_buffer(program_id: &Address, mint: &[u8; 32]) -> Option<&'static KnownBuffer> { - if program_id != &crate::ID { - return None; - } +/// The compile-time buffer for `mint`, if it's one of the known mints. +fn known_buffer(mint: &[u8; 32]) -> Option<&'static KnownBuffer> { let index = KNOWN_BUFFER_KEYS.binary_search(&search_key(mint)).ok()?; KNOWN_BUFFERS.get(index).filter(|known| &known.mint == mint) } @@ -127,7 +123,9 @@ fn known_buffer(program_id: &Address, mint: &[u8; 32]) -> Option<&'static KnownB /// /// A known mint is checked against its compile-time buffer alone, ignoring /// `bump`: nothing signs with a buffer's seeds, so the bump only matters for -/// re-deriving the address. +/// re-deriving the address. That buffer is derived under [`crate::ID`] whatever +/// `program_id` is, which is sound because the program only works there (see +/// [`crate::pda::state::STATE_PDA`]). #[inline] #[must_use = "ignoring the output means ignoring the validation result"] pub fn validate_buffer_pda( @@ -136,7 +134,7 @@ pub fn validate_buffer_pda( mint: &[u8; 32], bump: u8, ) -> Result<(), ProgramError> { - let is_buffer = match known_buffer(program_id, mint) { + let is_buffer = match known_buffer(mint) { Some(known) => buffer.as_array() == &known.address, None => { is_pda_with_signer_seeds(buffer, program_id, buffer_pda_signer_seeds(mint, &[bump])) @@ -220,12 +218,15 @@ mod tests { } #[test] - fn every_known_mint_has_a_known_buffer() { + fn every_known_mint_looks_up_its_own_buffer() { for mint in known_mints::KNOWN_MINTS { let mint = Pubkey::from_str_const(mint); - assert!( - known_buffer(&crate::ID, mint.as_array()).is_some(), - "{mint}" + let known = known_buffer(mint.as_array()) + .unwrap_or_else(|| panic!("{mint} must have a known buffer")); + assert_eq!(known.mint, *mint.as_array()); + assert_eq!( + known.address, + *find_buffer_pda(&crate::ID, &mint).0.as_array() ); } } @@ -234,12 +235,7 @@ mod tests { fn unknown_mint_sharing_a_search_key_has_no_known_buffer() { let mut mint = *USDC.as_array(); mint[31] ^= 1; - assert!(known_buffer(&crate::ID, &mint).is_none()); - } - - #[test] - fn known_mint_under_another_program_has_no_known_buffer() { - assert!(known_buffer(&Pubkey::new_unique(), USDC.as_array()).is_none()); + assert!(known_buffer(&mint).is_none()); } #[test] @@ -267,15 +263,6 @@ mod tests { .expect("the known buffer PDA must be accepted whatever the bump"); } - #[test] - fn derives_a_known_mint_buffer_under_another_program() { - let program_id = Pubkey::new_unique(); - let (pda, bump) = find_buffer_pda(&program_id, &USDC); - - validate_buffer_pda(&program_id, &pda, USDC.as_array(), bump) - .expect("the buffer PDA under another program must be accepted"); - } - mod proptest { use ::proptest::prelude::*; From 47648ac360f6f6abf49ca7d874e523ab932055cf Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:05:23 +0900 Subject: [PATCH 03/20] Find known buffers with a perfect hash The known mints are fixed at compile time, so a const block searches for a multiplier that sends each one to its own slot of a 256-entry index table. A lookup is now one multiply, shift and load plus the full mint comparison, instead of a 7-comparison binary search: 42 CU cheaper for known and unknown mints alike, and 744 bytes smaller. Co-Authored-By: Claude Opus 5.5 (1M context) --- bench-report.json | 20 +++--- interface/src/pda/buffer.rs | 123 ++++++++++++++++++++++++++++-------- 2 files changed, 106 insertions(+), 37 deletions(-) diff --git a/bench-report.json b/bench-report.json index b80fc18c..9967b036 100644 --- a/bench-report.json +++ b/bench-report.json @@ -79,16 +79,16 @@ "remove_solver/removes_a_solver": 489, "settle/finalizes_with_no_pushes": 1105, "settle/happy_path_sell_tokens_for_native_sol": 4613, - "settle/pulls_from_multiple_orders": 13396, - "settle/pulls_funds_to_destination": 7286, - "settle/pulls_to_multiple_destinations": 8438, - "settle/pushes_a_single_order": 6125, - "settle/pushes_a_single_order_of_a_known_mint": 4618, - "settle/pushes_several_orders_from_different_buffers": 11074, - "settle/pushes_several_orders_from_one_buffer": 11073, - "settle/settles_a_single_order": 6143, - "settle/settles_multiple_orders": 16076, - "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7280, + "settle/pulls_from_multiple_orders": 13310, + "settle/pulls_funds_to_destination": 7243, + "settle/pulls_to_multiple_destinations": 8395, + "settle/pushes_a_single_order": 6082, + "settle/pushes_a_single_order_of_a_known_mint": 4576, + "settle/pushes_several_orders_from_different_buffers": 10987, + "settle/pushes_several_orders_from_one_buffer": 10987, + "settle/settles_a_single_order": 6100, + "settle/settles_multiple_orders": 15950, + "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7236, "transfer_authority/manager_can_transfer_manager": 158, "transfer_authority/manager_can_transfer_reclaim_authority": 162, "transfer_authority/manager_can_transfer_settlement_owned_order_authority": 161, diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index d75dc5e2..acbe5070 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -58,8 +58,8 @@ struct KnownBuffer { address: [u8; 32], } -/// The buffers of [`known_mints::KNOWN_MINTS`], ordered by [`search_key`] so -/// the program can binary search [`KNOWN_BUFFER_KEYS`] instead of paying for a +/// The buffers of [`known_mints::KNOWN_MINTS`], in the same order, which the +/// program finds through [`KNOWN_BUFFER_SLOTS`] instead of paying for a /// `create_program_address` syscall. // Deriving this many PDAs in const eval trips the compiler's infinite-loop // guard, although it finishes in seconds. @@ -78,44 +78,85 @@ const KNOWN_BUFFERS: [KnownBuffer; known_mints::KNOWN_MINTS.len()] = { &buffer_pda_seeds(&mint), crate::ID.as_array(), ); - // Insertion sort: shift every buffer with a larger key up one slot. - let mut j = i; - while j > 0 && search_key(&buffers[j - 1].mint) > search_key(&mint) { - buffers.swap(j - 1, j); - j -= 1; - } - buffers[j] = KnownBuffer { mint, address }; + buffers[i] = KnownBuffer { mint, address }; i += 1; } buffers }; -/// The [`search_key`] of each of [`KNOWN_BUFFERS`], in the same order. -const KNOWN_BUFFER_KEYS: [u64; KNOWN_BUFFERS.len()] = { - let mut keys = [0; KNOWN_BUFFERS.len()]; +/// Bits of the hash that pick a slot: 256 slots keep collisions among 64 mints +/// rare enough that a multiplier turns up within a few thousand attempts. +const SLOT_BITS: u32 = 8; + +/// Gives every one of [`KNOWN_BUFFERS`] its own slot in [`KNOWN_BUFFER_SLOTS`]. +const KNOWN_BUFFER_MULTIPLIER: u64 = find_slot_multiplier(&KNOWN_BUFFERS); + +/// The index into [`KNOWN_BUFFERS`] of the mint in each slot, or `u8::MAX` for +/// an empty slot, which [`known_buffer`] then finds no buffer at. +const KNOWN_BUFFER_SLOTS: [u8; 1 << SLOT_BITS] = { + assert!( + KNOWN_BUFFERS.len() < u8::MAX as usize, + "too many known mints for u8 indices" + ); + let mut slots = [u8::MAX; 1 << SLOT_BITS]; let mut i = 0; - while i < keys.len() { - keys[i] = search_key(&KNOWN_BUFFERS[i].mint); - // Strictly increasing, so a key identifies at most one known buffer. - assert!( - i == 0 || keys[i - 1] < keys[i], - "known mints must have distinct search keys" - ); + while i < KNOWN_BUFFERS.len() { + slots[slot(&KNOWN_BUFFERS[i].mint, KNOWN_BUFFER_MULTIPLIER)] = i as u8; i += 1; } - keys + slots }; -/// The leading 8 bytes of `mint`: comparing a `u64` is cheaper on-chain than -/// comparing the whole address. -const fn search_key(mint: &[u8; 32]) -> u64 { - u64::from_le_bytes(*mint.first_chunk().expect("a mint is longer than 8 bytes")) +/// Multiplicative hash of the leading 8 bytes of `mint`, keeping the top +/// [`SLOT_BITS`] of the product. +const fn slot(mint: &[u8; 32], multiplier: u64) -> usize { + let key = u64::from_le_bytes(*mint.first_chunk().expect("a mint is longer than 8 bytes")); + (key.wrapping_mul(multiplier) >> (u64::BITS - SLOT_BITS)) as usize +} + +/// Try pseudo-random multipliers until one sends each of `buffers` to a +/// distinct [`slot`]. +/// +/// Panics (at compile time, for [`KNOWN_BUFFER_MULTIPLIER`]) if none does, +/// which is certain if two mints share their leading 8 bytes. +const fn find_slot_multiplier(buffers: &[KnownBuffer]) -> u64 { + const ATTEMPTS: u32 = 100_000; + let mut state = 0u64; + let mut attempt = 0; + while attempt < ATTEMPTS { + // splitmix64 + state = state.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut multiplier = state; + multiplier = (multiplier ^ (multiplier >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + multiplier = (multiplier ^ (multiplier >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + multiplier ^= multiplier >> 31; + + let mut taken = [false; 1 << SLOT_BITS]; + let mut i = 0; + while i < buffers.len() { + let slot = slot(&buffers[i].mint, multiplier); + if taken[slot] { + break; + } + taken[slot] = true; + i = i.checked_add(1).expect("i stays below the buffer count"); + } + if i == buffers.len() { + return multiplier; + } + attempt = attempt + .checked_add(1) + .expect("attempt stays below ATTEMPTS"); + } + panic!("no multiplier gives every known mint its own slot"); } /// The compile-time buffer for `mint`, if it's one of the known mints. fn known_buffer(mint: &[u8; 32]) -> Option<&'static KnownBuffer> { - let index = KNOWN_BUFFER_KEYS.binary_search(&search_key(mint)).ok()?; - KNOWN_BUFFERS.get(index).filter(|known| &known.mint == mint) + let index = KNOWN_BUFFER_SLOTS[slot(mint, KNOWN_BUFFER_MULTIPLIER)]; + KNOWN_BUFFERS + .get(usize::from(index)) + .filter(|known| &known.mint == mint) } /// Confirm `buffer` matches the derived buffer PDA for the mint bytes `mint` @@ -232,12 +273,40 @@ mod tests { } #[test] - fn unknown_mint_sharing_a_search_key_has_no_known_buffer() { + fn unknown_mint_sharing_a_slot_has_no_known_buffer() { let mut mint = *USDC.as_array(); mint[31] ^= 1; assert!(known_buffer(&mint).is_none()); } + #[test] + fn unknown_mint_in_an_empty_slot_has_no_known_buffer() { + let mint = (0u64..) + .map(|seed| *crate::fixtures::pubkey_from_seed(&seed.to_string()).as_array()) + .find(|mint| KNOWN_BUFFER_SLOTS[slot(mint, KNOWN_BUFFER_MULTIPLIER)] == u8::MAX) + .expect("most slots are empty"); + assert!(known_buffer(&mint).is_none()); + } + + /// `find_slot_multiplier` runs in a const context, where this panic is a + /// compile error; calling it at runtime is the only way to observe it. + #[test] + #[should_panic(expected = "no multiplier gives every known mint its own slot")] + fn find_slot_multiplier_rejects_mints_sharing_their_leading_bytes() { + let mut other = *USDC.as_array(); + other[31] ^= 1; + let _ = find_slot_multiplier(&[ + KnownBuffer { + mint: *USDC.as_array(), + address: [0; 32], + }, + KnownBuffer { + mint: other, + address: [0; 32], + }, + ]); + } + #[test] fn accepts_the_known_buffer() { let (pda, bump) = find_buffer_pda(&crate::ID, &USDC); From 73dc3cbd287c7da068263998720255729a1818a3 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:29:00 +0900 Subject: [PATCH 04/20] Draw slot multiplier candidates from SHA-256 The hand-rolled splitmix64 was hard to review without knowing it. The candidates are now the leading 8 bytes of SHA-256(0), SHA-256(1), ..., through the const-crypto dependency we already have. SHA-256 is much slower to evaluate at compile time, so the slot table grows to 512 entries: today's mints then need 41 candidates instead of 1,364, keeping the build free of long-running const eval warnings at a cost of 256 bytes. Co-Authored-By: Claude Opus 5.5 (1M context) --- bench-report.json | 6 ++-- interface/src/pda/buffer.rs | 55 ++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/bench-report.json b/bench-report.json index 9967b036..380635c9 100644 --- a/bench-report.json +++ b/bench-report.json @@ -84,11 +84,11 @@ "settle/pulls_to_multiple_destinations": 8395, "settle/pushes_a_single_order": 6082, "settle/pushes_a_single_order_of_a_known_mint": 4576, - "settle/pushes_several_orders_from_different_buffers": 10987, + "settle/pushes_several_orders_from_different_buffers": 10992, "settle/pushes_several_orders_from_one_buffer": 10987, "settle/settles_a_single_order": 6100, - "settle/settles_multiple_orders": 15950, - "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7236, + "settle/settles_multiple_orders": 15945, + "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7241, "transfer_authority/manager_can_transfer_manager": 158, "transfer_authority/manager_can_transfer_reclaim_authority": 162, "transfer_authority/manager_can_transfer_settlement_owned_order_authority": 161, diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index acbe5070..cc6d8d8a 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -84,9 +84,9 @@ const KNOWN_BUFFERS: [KnownBuffer; known_mints::KNOWN_MINTS.len()] = { buffers }; -/// Bits of the hash that pick a slot: 256 slots keep collisions among 64 mints -/// rare enough that a multiplier turns up within a few thousand attempts. -const SLOT_BITS: u32 = 8; +/// Bits of the hash that pick a slot: 512 slots keep collisions among 64 mints +/// rare enough that a multiplier turns up within a few dozen attempts. +const SLOT_BITS: u32 = 9; /// Gives every one of [`KNOWN_BUFFERS`] its own slot in [`KNOWN_BUFFER_SLOTS`]. const KNOWN_BUFFER_MULTIPLIER: u64 = find_slot_multiplier(&KNOWN_BUFFERS); @@ -114,34 +114,24 @@ const fn slot(mint: &[u8; 32], multiplier: u64) -> usize { (key.wrapping_mul(multiplier) >> (u64::BITS - SLOT_BITS)) as usize } -/// Try pseudo-random multipliers until one sends each of `buffers` to a -/// distinct [`slot`]. +/// Try the multipliers `SHA-256(0)`, `SHA-256(1)`, ... (each truncated to its +/// first 8 bytes) until one sends each of `buffers` to a distinct [`slot`]. /// /// Panics (at compile time, for [`KNOWN_BUFFER_MULTIPLIER`]) if none does, /// which is certain if two mints share their leading 8 bytes. const fn find_slot_multiplier(buffers: &[KnownBuffer]) -> u64 { const ATTEMPTS: u32 = 100_000; - let mut state = 0u64; - let mut attempt = 0; + let mut attempt: u32 = 0; while attempt < ATTEMPTS { - // splitmix64 - state = state.wrapping_add(0x9e37_79b9_7f4a_7c15); - let mut multiplier = state; - multiplier = (multiplier ^ (multiplier >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); - multiplier = (multiplier ^ (multiplier >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); - multiplier ^= multiplier >> 31; - - let mut taken = [false; 1 << SLOT_BITS]; - let mut i = 0; - while i < buffers.len() { - let slot = slot(&buffers[i].mint, multiplier); - if taken[slot] { - break; - } - taken[slot] = true; - i = i.checked_add(1).expect("i stays below the buffer count"); - } - if i == buffers.len() { + let digest = const_crypto::sha2::Sha256::new() + .update(&attempt.to_le_bytes()) + .finalize(); + let multiplier = u64::from_le_bytes( + *digest + .first_chunk() + .expect("a digest is longer than 8 bytes"), + ); + if has_distinct_slots(buffers, multiplier) { return multiplier; } attempt = attempt @@ -151,6 +141,21 @@ const fn find_slot_multiplier(buffers: &[KnownBuffer]) -> u64 { panic!("no multiplier gives every known mint its own slot"); } +/// Whether `multiplier` sends each of `buffers` to a different [`slot`]. +const fn has_distinct_slots(buffers: &[KnownBuffer], multiplier: u64) -> bool { + let mut taken = [false; 1 << SLOT_BITS]; + let mut i = 0; + while i < buffers.len() { + let slot = slot(&buffers[i].mint, multiplier); + if taken[slot] { + return false; + } + taken[slot] = true; + i = i.checked_add(1).expect("i stays below the buffer count"); + } + true +} + /// The compile-time buffer for `mint`, if it's one of the known mints. fn known_buffer(mint: &[u8; 32]) -> Option<&'static KnownBuffer> { let index = KNOWN_BUFFER_SLOTS[slot(mint, KNOWN_BUFFER_MULTIPLIER)]; From 19e5670f4538716a8c05f3016775b75d467a1ef8 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:11:09 +0900 Subject: [PATCH 05/20] Update programs/settlement/tests/common/token.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- programs/settlement/tests/common/token.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index a200dbd7..616c3d14 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -65,7 +65,6 @@ pub fn plant_mint(svm: &mut LiteSVM, mint: Pubkey, authority: &Pubkey) { let mut data = vec![0u8; Mint::LEN]; Mint { mint_authority: Some(*authority).into(), - decimals: 6, is_initialized: true, ..Default::default() } From fee716e29e02582413c10b0926d59cde2fe79efd Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:16:08 +0900 Subject: [PATCH 06/20] Update interface/src/pda/buffer.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/pda/buffer.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index cc6d8d8a..28b6e3b0 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -58,9 +58,7 @@ struct KnownBuffer { address: [u8; 32], } -/// The buffers of [`known_mints::KNOWN_MINTS`], in the same order, which the -/// program finds through [`KNOWN_BUFFER_SLOTS`] instead of paying for a -/// `create_program_address` syscall. +/// The buffers of [`known_mints::KNOWN_MINTS`], in the same order. // Deriving this many PDAs in const eval trips the compiler's infinite-loop // guard, although it finishes in seconds. #[allow(long_running_const_eval)] From 7603ca1b8609e0e0bc4a372a4c4c9bfddbe85c6e Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:20:43 +0900 Subject: [PATCH 07/20] Update interface/src/pda/buffer.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/pda/buffer.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index 28b6e3b0..e1b10da6 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -83,7 +83,15 @@ const KNOWN_BUFFERS: [KnownBuffer; known_mints::KNOWN_MINTS.len()] = { }; /// Bits of the hash that pick a slot: 512 slots keep collisions among 64 mints -/// rare enough that a multiplier turns up within a few dozen attempts. +/// The number of bits representing each input slot of `KNOWN_BUFFER_SLOTS`. +/// More bits make `KNOWN_BUFFER_SLOTS` exponentially larger, and therefore +/// increase the size of the program. However, more bits also make it easier to +/// find a multiplier with no collisions, and therefore reduce compilation +/// times. +/// +/// The problem of avoiding collision is the same as the birthday paradox. +/// As a rule of thumb, finding a multiplier requires an expected `e^(n²/(2m))` +/// iterations, where `n` is the number of known mints and m is 2^SLOT_BITS. const SLOT_BITS: u32 = 9; /// Gives every one of [`KNOWN_BUFFERS`] its own slot in [`KNOWN_BUFFER_SLOTS`]. From 98bac60b5fac3ec7fc4c364c56632f4ec50d6453 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:56:32 +0900 Subject: [PATCH 08/20] update justfile help comment --- Justfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Justfile b/Justfile index 2d6dde05..42dd8775 100644 --- a/Justfile +++ b/Justfile @@ -31,8 +31,7 @@ build: build-program # Runs all the generated code jobs generate: generate-js-client -# Refresh the most traded token mints whose buffer PDAs the settlement program -# derives at compile time. Not part of `generate`: it snapshots live market data. +# Refresh the most traded token mints for the quick access buffer lookup table. Pulls live market data. generate-known-mints: node interface/scripts/generate-known-mints.mjs From 745eeeca0b9562782d7e91ef23c66eeed6555593 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:10:16 +0900 Subject: [PATCH 09/20] remove the generation script entirely, for right now we check the list in --- Justfile | 4 -- interface/scripts/generate-known-mints.mjs | 45 ---------------------- interface/src/pda/buffer/known_mints.rs | 6 +-- 3 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 interface/scripts/generate-known-mints.mjs diff --git a/Justfile b/Justfile index 42dd8775..fa3049d7 100644 --- a/Justfile +++ b/Justfile @@ -31,10 +31,6 @@ build: build-program # Runs all the generated code jobs generate: generate-js-client -# Refresh the most traded token mints for the quick access buffer lookup table. Pulls live market data. -generate-known-mints: - node interface/scripts/generate-known-mints.mjs - # Builds the JS/TS client from IDL. [working-directory: 'programs/settlement/idl'] @generate-js-client: diff --git a/interface/scripts/generate-known-mints.mjs b/interface/scripts/generate-known-mints.mjs deleted file mode 100644 index 8456c43a..00000000 --- a/interface/scripts/generate-known-mints.mjs +++ /dev/null @@ -1,45 +0,0 @@ -// Writes `src/pda/buffer/known_mints.rs`: the mints of the most traded verified -// Solana tokens, whose buffer PDAs the interface derives at compile time so the -// settlement program can skip re-deriving them on-chain. -// -// Ranks Jupiter's verified token list by 24h volume. Jupiter's own `toptraded` -// ranking isn't used because it leaves out base tokens such as SOL and USDC. - -import { writeFileSync } from "node:fs"; - -const COUNT = 63; -const SOURCE = "https://lite-api.jup.ag/tokens/v2/tag?query=verified"; -const OUTPUT = new URL("../src/pda/buffer/known_mints.rs", import.meta.url); - -const response = await fetch(SOURCE); -if (!response.ok) { - throw new Error(`${SOURCE} answered ${response.status}`); -} -const tokens = await response.json(); - -const volume = ({ stats24h }) => - (stats24h?.buyVolume ?? 0) + (stats24h?.sellVolume ?? 0); -const ranked = tokens.sort((a, b) => volume(b) - volume(a)).slice(0, COUNT); -if (ranked.length !== COUNT) { - throw new Error( - `expected at least ${COUNT} verified tokens, got ${ranked.length}`, - ); -} - -// Symbols are free-form text; keep only what reads safely in a line comment. -const label = (symbol) => symbol.replace(/[^\x20-\x7e]/g, "").trim() || "?"; -const date = new Date().toISOString().slice(0, 10); -// Aligns the symbol comments the way rustfmt does. -const width = Math.max(...ranked.map(({ id }) => id.length)); - -writeFileSync( - OUTPUT, - `// @generated by \`just generate-known-mints\` on ${date}. Do not edit by hand. - -/// The ${COUNT} verified tokens with the highest 24h trading volume on Jupiter, -/// most traded first. -pub(super) const KNOWN_MINTS: [&str; ${COUNT}] = [ -${ranked.map(({ id, symbol }) => ` ${`"${id}",`.padEnd(width + 3)} // ${label(symbol)}`).join("\n")} -]; -`, -); diff --git a/interface/src/pda/buffer/known_mints.rs b/interface/src/pda/buffer/known_mints.rs index 6aa8753b..be95a1e5 100644 --- a/interface/src/pda/buffer/known_mints.rs +++ b/interface/src/pda/buffer/known_mints.rs @@ -1,7 +1,5 @@ -// @generated by `just generate-known-mints` on 2026-09-30. Do not edit by hand. - -/// The 63 verified tokens with the highest 24h trading volume on Jupiter, -/// most traded first. +/// The 63 verified tokens sampled by a snapshot of Jupiter's trading API +/// Most volume first. pub(super) const KNOWN_MINTS: [&str; 63] = [ "So11111111111111111111111111111111111111112", // SOL "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC From 1121afe3d398bfde927c0a211b1b3c1403dec3ab Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:13:03 +0900 Subject: [PATCH 10/20] sort addresses --- interface/src/pda/buffer/known_mints.rs | 104 ++++++++++++------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/interface/src/pda/buffer/known_mints.rs b/interface/src/pda/buffer/known_mints.rs index be95a1e5..347d12cf 100644 --- a/interface/src/pda/buffer/known_mints.rs +++ b/interface/src/pda/buffer/known_mints.rs @@ -1,67 +1,67 @@ /// The 63 verified tokens sampled by a snapshot of Jupiter's trading API -/// Most volume first. +/// Sorted by address for easier diffing pub(super) const KNOWN_MINTS: [&str; 63] = [ - "So11111111111111111111111111111111111111112", // SOL - "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC - "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT - "9BEcn9aPEmhSPbPQeFGjidRiEKki46fVQDyPpSQXPA2D", // jlUSDC - "Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh", // NVDAx + "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", // JLP "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", // PYUSD - "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", // PUMP - "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij", // cbBTC "2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH", // USDG - "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", // STONK - "DJTu7vi8norVzdVAffgvb39VP7wjKeTsgaMBJrzfxvoF", // DJT - "A7bdiYdS5GjqGFtxf17ppRHtDKPkkRqbKtR27dxvQXaS", // ZEC - "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", // SPCXx - "oPAiAikWTaFj9RYoRFD35ccfwhnMcB3ThgBZRHSkjTZ", // tOpenAI - "NFLX7qV57zuVxCoHy3s1jiGZyALraLNwttbxdvmYJLJ", // NFLX - "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", // ETH - "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB", // USD1 - "3ZLekZYq2qkZiSpnSvabjit34tUkjSwD1JFuW9as9wBG", // wNEAR - "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", // JUP - "JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD", // JupUSD "2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv", // PENGU - "98sMhvDwXj1RQi5c5Mndm3vPe9cBqPrbLaufMXFNMh5g", // HYPE - "Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk", // USELESS - "CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH", // CASH + "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", // WBTC + "3ZLekZYq2qkZiSpnSvabjit34tUkjSwD1JFuW9as9wBG", // wNEAR "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", // RAY - "XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W", // SPYx + "4sWNB8zGWHkh6UnmwiEtzNxL4XrN7uK9tosbESbJFfVs", // xSOL + "5Y8NV33Vv7WbnLfq3zBcKSdYPrk7g2KoiQoe7M2tcxp5", // ONyc + "5YMkXAYccHSGnHn9nob9xEvv6Pvka9DZWH7nTbotTu9E", // hyUSD + "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", // STONK "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", // TRUMP - "CARDSccUMFKoPRZxt5vt3ksUbxEFEcnZ3H2pd3dKxYjp", // CARDS - "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", // WBTC - "SPCXxcqXj6e5dJDVNovHN8744zkbhM2bYudU45BimGb", // SPCX - "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", // JLP - "BPxxfRCXkUVhig4HS1Lh7kZqV6SPJhzfEk4x6fVBjPCy", // BP - "RKLBnAXGqv31iZomqsuAWkQm1aqC7JwwvbCfzGdqAhz", // RKLB + "7GxATsNMnaC88vdwd2t3mwrFuQwwGvmYPrUQ4D6FotXk", // JUICED + "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", // ETH + "98sMhvDwXj1RQi5c5Mndm3vPe9cBqPrbLaufMXFNMh5g", // HYPE + "9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump", // Fartcoin + "9BEcn9aPEmhSPbPQeFGjidRiEKki46fVQDyPpSQXPA2D", // jlUSDC + "9cRCn9rGT8V2imeM2BaKs13yhMEais3ruM3rPvTGpump", // ANSEM + "9fvHrYNw1A8Evpcj7X2yy4k4fT7nNHcA9L6UsamNHAif", // jlUSDG + "A7bdiYdS5GjqGFtxf17ppRHtDKPkkRqbKtR27dxvQXaS", // ZEC "Ai66LHZG9MCzg1WKdawwqduVAXpNDUuV8M3uyq5ppump", // CATE - "SKHYhSjuRWHgikq8eRKbtBbpABgJSkd7ytQV14i9EQ3", // SKHY - "Xsa62P5mvPszXL1krVUnU5ar38bBSVcWAB6fmPCo5Zu", // METAx - "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", // JitoSOL - "Grass7B4RdKfBCjTKgSqnXkqjwiGvQyFbuSCUJr3XXjs", // GRASS - "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", // Bonk + "BPxxfRCXkUVhig4HS1Lh7kZqV6SPJhzfEk4x6fVBjPCy", // BP + "CARDSccUMFKoPRZxt5vt3ksUbxEFEcnZ3H2pd3dKxYjp", // CARDS + "CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH", // CASH + "CWZ6BsdnjkDVTGkmL6bGbJXXig6ceef12KvyGQW14cMt", // ANTFUN "DEkqHyPN7GMRJ5cArtQFAWefqbZb33Hyf6s5iCwjEonT", // USDe - "9cRCn9rGT8V2imeM2BaKs13yhMEais3ruM3rPvTGpump", // ANSEM - "Pren1FvFX6J3E4kXhJuCiAD5aDmGEb7qJRncwA8Lkhw", // ANTHROPIC + "DJTu7vi8norVzdVAffgvb39VP7wjKeTsgaMBJrzfxvoF", // DJT + "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", // Bonk + "DoGEV7LASBkQbibMc5k5vKnTZoMg423GpJ5QtJEGfm7R", // DOGE + "Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk", // USELESS + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC + "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT + "Grass7B4RdKfBCjTKgSqnXkqjwiGvQyFbuSCUJr3XXjs", // GRASS + "HTmQz7My6MehV7bjhJ6jde8nDND1yvsz68d24LP7YgUQ", // GP + "HcRLc9VDgjLeK154xDawfb1dmVJ98DoSqcwTHGqiDeJR", // ZCAT + "HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr", // EURC + "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", // JitoSOL + "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN", // JUP + "JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD", // JupUSD + "KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS", // KMNO "METvsvVRapdj9cFLzq4Tr43xK4tAjQfwX76z3n6mWQL", // MET "MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5", // MEW - "SNDKbwMUQvZhnLnxLduradgLHG5KrPuKwpnrkkGRhfH", // SNDK - "Xsv9hRk1z5ystj9MhnA7Lq4vjSsLwzL2nxrwmwtD3re", // GLDx - "KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS", // KMNO - "XsueG8BtpquVJX9LVLLEGuViXUungE6WmK5YZ3p3bd1", // CRCLx - "5Y8NV33Vv7WbnLfq3zBcKSdYPrk7g2KoiQoe7M2tcxp5", // ONyc - "4sWNB8zGWHkh6UnmwiEtzNxL4XrN7uK9tosbESbJFfVs", // xSOL - "WXMRyRZhsa19ety5erZhHg4N3xj3EVN92u94422teJp", // XMR - "HcRLc9VDgjLeK154xDawfb1dmVJ98DoSqcwTHGqiDeJR", // ZCAT - "9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump", // Fartcoin - "9fvHrYNw1A8Evpcj7X2yy4k4fT7nNHcA9L6UsamNHAif", // jlUSDG - "CWZ6BsdnjkDVTGkmL6bGbJXXig6ceef12KvyGQW14cMt", // ANTFUN - "5YMkXAYccHSGnHn9nob9xEvv6Pvka9DZWH7nTbotTu9E", // hyUSD "MUxEsUKSMACyw5fZf68wxf5FLnZVhtU9CwH8uNNGay1", // MU + "NFLX7qV57zuVxCoHy3s1jiGZyALraLNwttbxdvmYJLJ", // NFLX + "Pren1FvFX6J3E4kXhJuCiAD5aDmGEb7qJRncwA8Lkhw", // ANTHROPIC "PreweJYECqtQwBtpxHL171nL2K6umo692gTm7Q3rpgF", // OPENAI + "RKLBnAXGqv31iZomqsuAWkQm1aqC7JwwvbCfzGdqAhz", // RKLB + "SKHYhSjuRWHgikq8eRKbtBbpABgJSkd7ytQV14i9EQ3", // SKHY + "SNDKbwMUQvZhnLnxLduradgLHG5KrPuKwpnrkkGRhfH", // SNDK + "SPCXxcqXj6e5dJDVNovHN8744zkbhM2bYudU45BimGb", // SPCX + "So11111111111111111111111111111111111111112", // SOL + "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB", // USD1 + "WXMRyRZhsa19ety5erZhHg4N3xj3EVN92u94422teJp", // XMR + "Xs3oZwbHvqis4NYcf4YKWmEia2eC84wSiVrcYcTqpH8", // SPCXx + "Xsa62P5mvPszXL1krVUnU5ar38bBSVcWAB6fmPCo5Zu", // METAx + "Xsc9qvGR1efVDFGLrVsmkzv3qi45LTBjeUKSPmx9qEh", // NVDAx + "XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W", // SPYx "XspzcW1PRtgf6Wj92HCiZdjzKCyFekVD8P5Ueh3dRMX", // MSFTx - "HTmQz7My6MehV7bjhJ6jde8nDND1yvsz68d24LP7YgUQ", // GP - "7GxATsNMnaC88vdwd2t3mwrFuQwwGvmYPrUQ4D6FotXk", // JUICED - "HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr", // EURC - "DoGEV7LASBkQbibMc5k5vKnTZoMg423GpJ5QtJEGfm7R", // DOGE + "XsueG8BtpquVJX9LVLLEGuViXUungE6WmK5YZ3p3bd1", // CRCLx + "Xsv9hRk1z5ystj9MhnA7Lq4vjSsLwzL2nxrwmwtD3re", // GLDx + "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij", // cbBTC + "oPAiAikWTaFj9RYoRFD35ccfwhnMcB3ThgBZRHSkjTZ", // tOpenAI + "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", // PUMP ]; From fd51f2b93d00e9a46fe15f4ed893c8fa38a30b3b Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:17:01 +0900 Subject: [PATCH 11/20] explain why sha256 is used --- interface/src/pda/buffer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index e1b10da6..d8fa1c97 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -123,6 +123,8 @@ const fn slot(mint: &[u8; 32], multiplier: u64) -> usize { /// Try the multipliers `SHA-256(0)`, `SHA-256(1)`, ... (each truncated to its /// first 8 bytes) until one sends each of `buffers` to a distinct [`slot`]. /// +/// SHA-256 is utilized to provide a pseudorandom source of multipliers, but any PRNG would work fine here. +/// /// Panics (at compile time, for [`KNOWN_BUFFER_MULTIPLIER`]) if none does, /// which is certain if two mints share their leading 8 bytes. const fn find_slot_multiplier(buffers: &[KnownBuffer]) -> u64 { From dced252d246e4ea0008f010325d872ff2d7cbae2 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:21:18 +0900 Subject: [PATCH 12/20] Update interface/src/pda/buffer.rs Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/pda/buffer.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index d8fa1c97..240561b8 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -278,10 +278,6 @@ mod tests { let known = known_buffer(mint.as_array()) .unwrap_or_else(|| panic!("{mint} must have a known buffer")); assert_eq!(known.mint, *mint.as_array()); - assert_eq!( - known.address, - *find_buffer_pda(&crate::ID, &mint).0.as_array() - ); } } From fb76895625a4fca32b13683f05d6cc9462a3f7c1 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:28:09 +0900 Subject: [PATCH 13/20] setup known mints to be usable by the tests --- interface/src/pda/buffer.rs | 31 +++++++++++-------- interface/src/pda/buffer/known_mints.rs | 2 +- .../tests/finalize_settle_pushes.rs | 5 ++- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index d8fa1c97..f3b49f4e 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -27,6 +27,7 @@ use crate::pda::{is_pda_with_signer_seeds, SETTLEMENT_SEED}; use crate::SettlementError; mod known_mints; +pub use known_mints::KNOWN_MINTS; /// Trailing seed identifying the buffer PDAs. pub const BUFFER_SEED: &[u8] = b"buffer"; @@ -259,8 +260,7 @@ mod tests { assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); } - /// The USDC mint, one of [`known_mints::KNOWN_MINTS`]. - const USDC: Pubkey = Pubkey::from_str_const("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + const A_KNOWN_MINT: Pubkey = Pubkey::from_str_const(KNOWN_MINTS[0]); #[test] fn known_buffers_are_canonical() { @@ -287,7 +287,7 @@ mod tests { #[test] fn unknown_mint_sharing_a_slot_has_no_known_buffer() { - let mut mint = *USDC.as_array(); + let mut mint = *A_KNOWN_MINT.as_array(); mint[31] ^= 1; assert!(known_buffer(&mint).is_none()); } @@ -306,11 +306,11 @@ mod tests { #[test] #[should_panic(expected = "no multiplier gives every known mint its own slot")] fn find_slot_multiplier_rejects_mints_sharing_their_leading_bytes() { - let mut other = *USDC.as_array(); + let mut other = *A_KNOWN_MINT.as_array(); other[31] ^= 1; let _ = find_slot_multiplier(&[ KnownBuffer { - mint: *USDC.as_array(), + mint: *A_KNOWN_MINT.as_array(), address: [0; 32], }, KnownBuffer { @@ -322,26 +322,31 @@ mod tests { #[test] fn accepts_the_known_buffer() { - let (pda, bump) = find_buffer_pda(&crate::ID, &USDC); + let (pda, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT); - validate_buffer_pda(&crate::ID, &pda, USDC.as_array(), bump) + validate_buffer_pda(&crate::ID, &pda, A_KNOWN_MINT.as_array(), bump) .expect("the known buffer PDA must be accepted"); } #[test] fn rejects_an_invalid_address_for_a_known_mint() { - let (_, bump) = find_buffer_pda(&crate::ID, &USDC); - - let err = validate_buffer_pda(&crate::ID, &Pubkey::new_unique(), USDC.as_array(), bump) - .expect_err("a non-canonical address must be rejected"); + let (_, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT); + + let err = validate_buffer_pda( + &crate::ID, + &Pubkey::new_unique(), + A_KNOWN_MINT.as_array(), + bump, + ) + .expect_err("a non-canonical address must be rejected"); assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); } #[test] fn ignores_the_bump_for_a_known_mint() { - let (pda, bump) = find_buffer_pda(&crate::ID, &USDC); + let (pda, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT); - validate_buffer_pda(&crate::ID, &pda, USDC.as_array(), bump ^ 1) + validate_buffer_pda(&crate::ID, &pda, A_KNOWN_MINT.as_array(), bump ^ 1) .expect("the known buffer PDA must be accepted whatever the bump"); } diff --git a/interface/src/pda/buffer/known_mints.rs b/interface/src/pda/buffer/known_mints.rs index 347d12cf..d5f4498c 100644 --- a/interface/src/pda/buffer/known_mints.rs +++ b/interface/src/pda/buffer/known_mints.rs @@ -1,6 +1,6 @@ /// The 63 verified tokens sampled by a snapshot of Jupiter's trading API /// Sorted by address for easier diffing -pub(super) const KNOWN_MINTS: [&str; 63] = [ +pub const KNOWN_MINTS: [&str; 63] = [ "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", // JLP "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", // PYUSD "2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH", // USDG diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index 25533ff0..c6fc4ccc 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -22,7 +22,7 @@ use cow_settlement_client::instruction::{FinalizeSettle, FinalizedIntent}; use cow_settlement_client::{ cow_settlement_interface::{ data::intent::{Asset, OrderIntent, TokenAsset}, - pda::state::STATE_PDA, + pda::{buffer::KNOWN_MINTS, state::STATE_PDA}, Instruction, SettlementError, }, instruction::TokenProgram, @@ -83,8 +83,7 @@ fn pushes_a_single_order() { #[test] fn pushes_a_single_order_of_a_known_mint() { let (mut svm, program_id, payer, solver) = setup_settle_ready(); - // USDC, one of the mints whose buffer the program knows at compile time. - let mint = Pubkey::from_str_const("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + let mint = Pubkey::from_str_const(KNOWN_MINTS[0]); token::plant_mint(&mut svm, mint, &payer.pubkey()); let intent = OrderBuilder::new(&mut svm, &program_id, &payer) .buy_mint(&mint) From e6825de23b06796bb3d4ff6b535338625ae6fa13 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:45:30 +0900 Subject: [PATCH 14/20] add descriptive comment --- interface/src/pda/buffer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index c4fa7969..1acace85 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -101,6 +101,7 @@ const KNOWN_BUFFER_MULTIPLIER: u64 = find_slot_multiplier(&KNOWN_BUFFERS); /// The index into [`KNOWN_BUFFERS`] of the mint in each slot, or `u8::MAX` for /// an empty slot, which [`known_buffer`] then finds no buffer at. const KNOWN_BUFFER_SLOTS: [u8; 1 << SLOT_BITS] = { + // Strictly less than MAX because the value is reserved for no buffer. assert!( KNOWN_BUFFERS.len() < u8::MAX as usize, "too many known mints for u8 indices" From 3a6e5f77df6411f5fd16679ec7d380158492feea Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:47:12 +0900 Subject: [PATCH 15/20] separate test for known buffers == known mints len --- interface/src/pda/buffer.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index 1acace85..a18a8e27 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -264,8 +264,12 @@ mod tests { const A_KNOWN_MINT: Pubkey = Pubkey::from_str_const(KNOWN_MINTS[0]); #[test] - fn known_buffers_are_canonical() { + fn known_buffers_len_maps_known_mints() { assert_eq!(KNOWN_BUFFERS.len(), known_mints::KNOWN_MINTS.len()); + } + + #[test] + fn known_buffers_are_canonical() { for known in &KNOWN_BUFFERS { let (pda, _) = find_buffer_pda(&crate::ID, &Pubkey::new_from_array(known.mint)); assert_eq!(known.address, *pda.as_array()); From 4dc726e31f7b90d6284fd31fc23524731bcc74b7 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:52:16 +0900 Subject: [PATCH 16/20] add test condition to verify sharing slot case --- interface/src/pda/buffer.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index a18a8e27..ac62d58c 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -290,6 +290,14 @@ mod tests { fn unknown_mint_sharing_a_slot_has_no_known_buffer() { let mut mint = *A_KNOWN_MINT.as_array(); mint[31] ^= 1; + + // ensure that the slot we will hit is still resolving to the original mint + assert_ne!( + KNOWN_BUFFER_SLOTS[slot(&mint, KNOWN_BUFFER_MULTIPLIER)], + u8::MAX + ); + + // even so it should resolve to none assert!(known_buffer(&mint).is_none()); } From 7f4e71619de035d9078a6df8475765b56de996ba Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:12:28 +0900 Subject: [PATCH 17/20] remove `program_id` from the arguments for validate_buffer_pda we are moving in that direction --- DESIGN.md | 389 ------------------ interface/src/pda/buffer.rs | 31 +- .../settlement/src/processor/begin_settle.rs | 2 +- 3 files changed, 12 insertions(+), 410 deletions(-) delete mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md deleted file mode 100644 index 375f2d10..00000000 --- a/DESIGN.md +++ /dev/null @@ -1,389 +0,0 @@ -# Solana program high-level specs - -There's a single dedicated settlement program, deployed once and used for executing all settlements. - -The settlement program stores funds through dedicated token accounts (_buffer accounts_), one per token. - -It uses a dedicated state account to: - -- Store the [authorities](#authorities) of the protocol. -- Store the accounts that can execute a settlement (the _solvers_). -- Act as a token delegate to manage user funds. - -Its state is stored in a PDA generated using a seed based on the cargo package version, like `["settlement v0.1"]`. - -Once the testing phase has ended, we will make the code at that account unchangeable. - -## State versioning - -Every PDA the program derives starts with the same prefix seed: the string `settlement`, then the current cargo package major and minor version prefixed by `v`, space-padded to a fixed width to prevent accidental prefix collisions. While we expect the ultimate settlement program to be immutable so this measure would normally not be needed, during development we want to avoid any possible confusion or losses that could occur as a result of stale data still existing within the settlement program. - -Bumping the minor version relocates the program's entire account storage at once — the state account, every buffer, and every order. - -The state PDA's address and bump are pinned at compile time against the declared program ID, so handlers compare the account against a constant instead of deriving it on-chain. As a safeguard, `Initialize` still derives it, and it rejects a state account that doesn't match the constant. - -A bump is not a migration. There are some other consequences that should be considered before the new program version is deployed: - -- **User delegations stop working.** Users delegate their token accounts to the state PDA (see [user delegation](#user-delegation-ie-approvals)). A bump moves that address, so every user has to delegate again before they can trade. -- **Whatever the buffers still hold is stranded.** A buffer's funds are only spendable by the state PDA that is its SPL authority. After a bump the program can no longer sign for the old state PDA, so it can never move those funds again. Buffers must be drained under the old program version *before* deploying a bump. - -## Program initialization - -The state PDA is created once, after deployment, by the `Initialize` instruction. - -## Authorities - -The program grant privileged roles to specific accounts (_authorities_). They are: - -- Manager: the account that can update the address of all other roles. -- Solver Authority: the account that can add and remove solvers. -- Reclaim Authority: the account authorized to close buffer accounts, reclaim their rent, and choose where that rent goes. -- Settlement-Owned-Order Authority: the account authorized to place arbitrary orders that sell the protocol's own buffer balances (for fee withdrawals). - -### Updating authorities - -Any authority that holds a role can transfer it to another account with the `TransferAuthority` instruction, which sets the role's holder to the new account in a single step. The manager can transfer any role. - -## Buffer accounts - -Buffer accounts are token accounts that hold funds on behalf of the settlement program through the state PDA. - -These token accounts are accessible to all solvers and effectively work like the current buffers. They are used to send out funds to the user and collect fees, which stay on the buffers after the settlement. This means that the current fee accounting and withdrawal mechanism would be based on balance changes (like on Ethereum). - -The buffer account for native SOL is the state PDA itself, using the funds on top of the necessary rent. - -Corresponding PDAs are generated using seed `[SETTLEMENT_SEED, token, "buffer"]`. - -A buffer (except for the state PDA) is closed by the `ReclaimBuffer` instruction, which only the [reclaim authority](#authorities) can call. - -Differences with Ethereum: - -- In a settlement, a solver can't access buffers directly but needs to invoke a dedicated instruction or use the funds in them to pay out the proceeds of an order. -- Traded funds aren't sent automatically to the buffers at the start of the trade. Solvers can specify arbitrary receiver accounts for the user's sell tokens, as well as send the trade proceeds from any arbitrary account. In practice, buffers may still be used by solvers for pooling funds before interactions for efficiency reasons. - -## Solver authentication - -Solver authentication is managed by the settlement program. - -There are two roles for authentication: - -- The _solvers_, accounts that can execute settlements and withdraw from the buffers. -- The _solver authority_, an account that can add and remove solvers, as well as transfer its own role to another account. - -The settlement program state PDA stores the state used for authentication. - -On settlement program deployment, the program state is initialized with fixed initial authorities, controlled by CoW Core as mandated by CoW DAO, and an empty list of solvers. - -Differences with Ethereum: - -- There's no dedicated authenticator contract, the settlement program performs the role of the authenticator contract. This also means that the authenticator isn't upgradable: there's no `owner`, only a set of authorities. - -Limitation: - -- Storing all solvers on the same account limits the amount of possible solver accounts to ~64k. - -## Fee withdrawals - -Fees accumulate in the buffer accounts after a settlement is concluded. - -Fees are withdrawn by placing an order, owned by the settlement state PDA, that sells tokens stored in a buffer. Order creation is gated by the dedicated [settlement-owned-order authority](#authorities). - -The order is placed through the `CreateSettlementOwnedOrder` instruction. The settlement-owned-order authority can specify arbitrary order parameters, as long as the owner is the state PDA and the order is marked as created on-chain. - -Differences with Ethereum: - -- Solvers can't access the content of the buffers directly anymore (though they can do so indirectly by creating a dedicated order just to sweep the buffer). -- `CreateSettlementOwnedOrder` has a dedicated authority rather than requiring a solver. -- Creating a fee-withdrawal order is done directly through a dedicated instruction, not indirectly as the result of a call from the settlement context. - -## User delegation (i.e., "approvals") - -As we'll see, trades work through signed intents. - -A user who wants to use CoW Protocol for a token needs to set their [delegate](https://solana.com/docs/tokens/basics/approve-delegate) for that token to the settlement state PDA. - -Similar to Ethereum's approvals, this needs to be done by each user once per token in order to trade. - -As long as the settlement program is immutable, there's no other way to access user funds except through the execution of an order. - -Differences with Ethereum: - -- There can only be a single delegate. This could create situations where the user delegates the settlement state PDA, creates an order, and then another dapp delegates a different program and the order can't be settled anymore. This is very different from approvals, where an approval for one dapp doesn't affect approvals for other dapps. -- There's no dedicated vault relayer, the user delegates their tokens to the settlement state PDA (_not_ the settlement program!). This is because "interactions" aren't executed by the settlement program but as dedicated instructions originating from the transaction signer. - -## Orders - -### Intents - -Users interact with the protocol by [signing](#authenticating-an-order) an order intent off-chain. - -An order intent is the following list of parameters: - -```rust -struct OrderIntent { - owner: Pubkey - - // Origin to pull funds for the order. Only SPL or Token-2022 token programs are supported. - sell: TokenAsset { - mint: Pubkey, - token_account: Pubkey, - } - - // Destination to push proceeds of the order. On top of the token support in sell, native SOL (lamports) may be purchased - buy: TokenAsset { - mint: Pubkey, - token_account: Pubkey, - } | NativeAsset { - account: Pubkey, - } - - // Amounts are interpreted as exact or maximum depending on kind. - sell_amount: u64 - buy_amount: u64 - - // Unix timestamp - valid_to: u32 - flags: Flags - - // Usual app data field, it isn't directly used in the program. - app_data: [u8; 32] -} - -struct Flags { - // Indicates the path by which the order was created. Important for reclaim. - created_on_chain: bool - // Either Buy or Sell - kind: OrderKind - partially_fillable: bool -} -``` - -The sell and buy tokens are effectively flattened down in wire format, and in the case that `buy` uses `NativeAsset`, -the `buy_mint` is set to the Solana system program. - -The fields grouped in `Flags` share a single byte in the encoded form, one bit -each, with the remaining bits reserved and required when decoding to be zero. - -Differences with Ethereum: - -- In Solana, the spender token account (and the owner) is part of the intent, while in Ethereum it is implied in the signature. -- The `receiver` field in Ethereum is captured by the field `buy_token_account`. -- Sell and buy amounts are 64 bits instead of Ethereum's 256. - -### Orders are accounts - -For processing an order in a settlement, the data of that order needs to be stored in a dedicated account. Storing this data is, in general, the responsibility of the solver who settles the order the first time, but anyone can do it if a user signed an order. - -Useful information can be recovered from the order PDA. Notably: - -```rust -// The PDA's canonical bump, stored so instructions don't have to carry it -bump: u8 -cancelled: bool -amount_withdrawn: u64 -amount_received: u64 -// The account that created this order, used for refunding rent -created_by: Pubkey -intent: OrderIntent -``` - -`amount_withdrawn` and `amount_received` are cumulative across all of the order's settlements. They cap how much of the order can still be traded, so it can't be filled beyond its sell and buy amounts. - -An order PDA can only exist and hold data if the order has been [authenticated](#authenticating-an-order). If this account exists and is not cancelled, filled, or expired, then the order can be traded. - -At the time of order creation, the executor can specify a different address as the `created_by` address. This allows rent to be reclaimed by a different account than the one that signed the instruction. - -Corresponding PDAs are generated using seed `[SETTLEMENT_SEED, hash(intent), "order"]`. - -The serialization of the parameters before hashing and the hashing function will be formally specified at a later point. - -### Identifying an order (order UID) - -We use the hash of the intent parameters to represent the order, `hash(parameters)`. This is the same hash used to generate the order PDA. - -Differences with Ethereum: - -- Solana's order UIDs are 32 bytes, compared to 56 bytes in Ethereum. This is because: - - The owner isn't added to the UID. The owner is already included in the parameters, unlike in Ethereum, thus the owner doesn't need to be appended for disambiguation. - - The expiration isn't added to the UID. In Ethereum it was only added because of state clearing, here it isn't needed. - -### Cancelling an order - -Cancelling an order is an operation executed by the user to make it impossible to trade that order in the protocol. - -Cancelling an order requires an on-chain operation. This operation can be authenticated in two ways: - -- Directly, through the `CancelOrder` instruction signed by the order owner account. -- By anyone through a signed intent, signing the following cancellation struct: - ```rust - struct CancelIntent { - intent: OrderIntent - } - ``` - -Creating the order in advance is _not_ needed: if the order wasn’t created before cancelling, the corresponding order PDA is created and then cancelled. - -Note that deleting the order PDA is _not_ enough to cancel an order for off-chain orders. In fact, if an order signature is available, the same order could always be created again until it expires. - -### Order clearing - -Allocating an order PDA requires paying rent. - -If the order is expired, anyone can close the order account through the `ReclaimOrder` instruction. On account closure, the rent is sent to the order's `created_by` account, i.e., the original creator of the order. - -If an order created on-chain (`created_on_chain`), it can safely be closed earlier. In this case, `ReclaimOrder` will additionally allow reclaiming of orders that are cancelled or completely filled. - -This is useful for solvers who need to allocate the order for executing it, but the allocation itself would be orders of magnitude more expensive than the compute cost for executing an instruction. This is particularly relevant to make small orders economically viable. - -## Authenticating an order - -We want to support two ways to authenticate that an order comes from a user: - -- [Off-chain] Through an [Ed25519 signature](#ed25519), encoded as Solana’s off-chain message. -- [On-chain] In an [instruction](#on-chain-order-creation) by the owner that directly creates the order PDA. - -### Encoding the signed data - -The data to sign is encoded using Solana’s [off-chain message signing standard](https://docs.anza.xyz/proposals/off-chain-message-signing). It’s similar to the ERC-712 standard to encode structured data, in that it guarantees that: - -- Users cannot sign Solana transactions instead of messages by accident. -- The signature cannot be used by other protocols that rely on the same signature encoding: there is strict namespacing of signatures, and each signed message is only valid for the intended purpose. - -Differences with Ethereum: - -- Off-chain message signing seems to be much less supported by wallets compared to ERC-712. For example, there doesn’t seem to be a Phantom wallet API to encode off-chain messages (see [documentation](https://docs.phantom.com/sdks/browser-sdk/sign-messages), Solana only has a simple `signMessage`). Further research is needed to determine wallet compatibility, but there’s reason to believe it’s very low and it will compromise user signing experience. -- Off-chain message signing doesn’t have a native representation of structured data. Only UTF8 and ASCII data can be signed, meaning that even if wallets were supporting this standard, many of them would just present, in the best case, a sequence of bytes. - -### Ed25519 - -Raw Ed25519 signatures are supported by all native Solana accounts. - -The data to be signed is encoded as an off-chain message and signed with raw Ed25519 signatures. - -Orders that are created by this path must specify the flag `created_on_chain = false`. - -Differences with Ethereum: - -- Unlike ECDSA signatures in Ethereum, the owner account address cannot be recovered from the Ed25519 signature. This means that the address needs to be included as part of the signed data. -- Signatures are 64 bytes (unlike EVM’s 65). - -### On-chain order creation - -Orders can be created by the owner by executing an instruction on-chain. - -The order owner executes the `CreateOrder` instruction. The settlement program checks that the order comes from the owner and [creates the order PDA](#orders-are-accounts). - -In this authentication flow, the user needs to pay for the rent in SOL necessary to create the PDA. Note that the rent may be significantly higher than the expected trading fee. The rent can be recovered by the user once the order has expired by [clearing the order](#order-clearing). - -Orders that are created by this path must specify the flag `created_on_chain = true`. - -This flow supports both standard ("on-curve") accounts and PDA signatures. - -This flow is the only one allowing on-chain programs to trade through the settlement program. - -Advantages: - -- Smart wallets can sign an order without needing an Ed25519 private key. - -Disadvantages: - -- Not intent-based, the user needs to sign and execute an instruction for every order. - -Differences with Ethereum: - -- The rent refund process isn’t part of Ethereum and adds extra costs for the user. -- Unlike Ethereum’s pre-signing, this flow isn’t considered a "signature scheme" but it’s a direct way to publish in a trusted way all order information on-chain. This is because, in Solana, it’s more complex and expensive to manage dedicated storage for pre-signatures than just creating the order on-chain. - -## Settlements - -### A settlement - -A settlement is composed of: - -- The orders to settle. -- The list of token transfers to perform from the orders’ sell token accounts and to the orders’ buy token account. -- A list of interactions to execute. - -Security guarantees: - -- User: - - Funds can only be taken in the context of a signed order. - - No more than the order’s sell amount can be taken from the user. - - The user receives at least the specified buy amount (proportional to how much of the sell amount was used in the settlement). -- Protocol: - - Only approved solvers can settle. - -Differences with Ethereum: - -- There’s no per-settlement list of prices. -- Fund transfers are explicit instead of being automatically done as part of the order inclusion. - -### The settlement transaction - -A settlement transaction is split into multiple instructions. All settlement operations occur between a `BeginSettle` and a `FinalizeSettle` instruction with the exception of arbitrary interactions, which can take place at any point of a transaction. Except for that, the order of instructions in the transaction is arbitrary. - -- `BeginSettle`: Pulls funds from each order’s sell token account to the solver-specified destination accounts, using the settlement state PDA’s token delegation. Validates each order's limit price and that its cumulative fill stays within the order's sell and buy amounts (fully filling a fill-or-kill order), and updates the order's `amount_withdrawn`/`amount_received`. Carries an explicit `finalize_ix_index` pointing to its paired `FinalizeSettle`. -- (arbitrary interactions): Any instruction from the solver. This could be a token transfer, an AMM swap, or anything else. -- `FinalizeSettle`: Pushes the proceeds of each order from the settlement’s buffer accounts to the order’s buy token account, using the settlement state PDA’s authority over the buffers. Carries an explicit `begin_ix_index` pointing to its paired `BeginSettle`. - -Additionally, `BeginSettle` includes the `auction_id` (an `i64`) as part of its instruction data. This value is unused by the program and only relied upon by the off-chain back-end services. - -Only a single settlement can be executed at a time. This means that there can’t be a `BeginSettle` instruction or a spurious `FinalizeSettle` instruction between a coupled pair of `Begin`/`FinalizeSettle`. - -Differences with Ethereum: - -- Interactions aren’t executed from the context of the settlement program but from the context of the solver as completely separate instructions. - - Notably: a solver doesn’t have to set approvals from the settlement contract to on-chain contracts. Once solver privileges are removed, there’s no way for old solvers to access the buffers anymore, unlike in the current Ethereum contract. -- Fund transfers are explicit instead of being automatically done as part of the order inclusion. These transfers aren’t automatically done to the buffers, they may go to different accounts. - -## Selling SOL (a.k.a. ETH flow) - -Selling SOL is supported through a separate program (called SOL flow). - -Selling native tokens requires taking control of the user funds and wrapping them. - -While the technical details are different, for the user this will be very similar to the ETH flow experience. - -Differences with Ethereum: - -- Minimum order size: needed to pay for rent by the user when the order is created. (But the rent will be used in the trade, there won’t be leftovers or need to recover this SOL by the user.) -- No unlimited order deadline. -- Different execution flow for solvers compared to EVM. - -### Creating a SOL sell order (user facing) - -The user interacts with the SolFlow program through the instruction `CreateSolOrder`. This instruction takes the traded amount of SOL and the data describing a [user intent](#intents) minus the owner (implicitly assumed to be the instruction signer), the sell token account, and the amount (assumed to be the SOL amount in the instruction). - -This will create an SPL token account for WSOL, owned by the SolFlow PDA, with seeds equal to (`intent`, `owner`), storing the intent data and the user’s lamports. We call this the _custodial_ account of the user for that intent. - -The token account will be created automatically with the delegate set to the settlement program. - -### Enabling a SOL sell order (solver facing) - -Any unprivileged participant can interact with the SolFlow program to have it create an Order in the Settlement program using the funds in the dedicated custodial account. We call that unprivileged participant the _enabler_. - -The enabler calls the `SetUpOrder` instruction passing on the user accounts (custodial and buy token accounts) and the user order intent. - -Then, the `SolFlow` processor creates an order on the `SettlementProgram` using a modified version of the original intent: - -- Order owner becomes the `SolFlow` PDA. -- Sell token becomes the _custodial_ token account. -- Order `created_by` field is set to enabler’s address. - -The `SetUpOrder` validates the given intent by successfully re-deriving the custodial account. - -### Recovering funds by the user - -Each intermediate step can be reverted by the original owner to recover the funds, as long as the order hasn’t been executed. It supports partial execution. - -### Comparison with direct wrapping and order creation - -Wrapping and creating an order is easier in Solana and everything can be executed in the same transaction. The delegation instruction can be bundled as well. - -The main reason to prefer the SOL flow described here is the handling of the rent amount. When wrapping and creating an order, the user needs to pay rent possibly for creating the order account. This is particularly notable for small orders: the order account would have to be refunded later to recover the user’s SOL. - -## Token 2022 - -The settlement program natively supports [Token-2022](https://www.solana-program.com/docs/token-2022) tokens. The settlement program does not explicitly block any -token extensions, but if the extension interferes with the settlement process such that it causes a failure (such as locked tokens), then any affected orders will -be unsettleable. diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index ac62d58c..3e0201ec 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -185,7 +185,6 @@ fn known_buffer(mint: &[u8; 32]) -> Option<&'static KnownBuffer> { #[inline] #[must_use = "ignoring the output means ignoring the validation result"] pub fn validate_buffer_pda( - program_id: &Address, buffer: &Address, mint: &[u8; 32], bump: u8, @@ -193,7 +192,7 @@ pub fn validate_buffer_pda( let is_buffer = match known_buffer(mint) { Some(known) => buffer.as_array() == &known.address, None => { - is_pda_with_signer_seeds(buffer, program_id, buffer_pda_signer_seeds(mint, &[bump])) + is_pda_with_signer_seeds(buffer, &crate::ID, buffer_pda_signer_seeds(mint, &[bump])) } }; is_buffer @@ -226,37 +225,34 @@ mod tests { #[test] fn accepts_a_valid_address() { - let program_id = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let (pda, bump) = find_buffer_pda(&program_id, &mint); + let (pda, bump) = find_buffer_pda(&crate::ID, &mint); let buffer = crate::instruction::fixtures::fake_account(pda); - validate_buffer_pda(&program_id, buffer.address(), mint.as_array(), bump) + validate_buffer_pda(&buffer.address(), mint.as_array(), bump) .expect("the canonical buffer PDA must be accepted"); } #[test] fn rejects_an_invalid_address() { - let program_id = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let (_, bump) = find_buffer_pda(&program_id, &mint); + let (_, bump) = find_buffer_pda(&crate::ID, &mint); // An account sitting at some other address is not the buffer. let buffer = crate::instruction::fixtures::fake_account(Pubkey::new_unique()); - let err = validate_buffer_pda(&program_id, buffer.address(), mint.as_array(), bump) + let err = validate_buffer_pda(&buffer.address(), mint.as_array(), bump) .expect_err("a non-canonical address must be rejected"); assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); } #[test] fn rejects_a_wrong_bump() { - let program_id = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let (pda, bump) = find_buffer_pda(&program_id, &mint); + let (pda, bump) = find_buffer_pda(&crate::ID, &mint); // The address is canonical but the carried bump doesn't derive it. let buffer = crate::instruction::fixtures::fake_account(pda); - let err = validate_buffer_pda(&program_id, buffer.address(), mint.as_array(), bump ^ 1) + let err = validate_buffer_pda(&buffer.address(), mint.as_array(), bump ^ 1) .expect_err("a wrong bump must be rejected"); assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); } @@ -333,7 +329,7 @@ mod tests { fn accepts_the_known_buffer() { let (pda, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT); - validate_buffer_pda(&crate::ID, &pda, A_KNOWN_MINT.as_array(), bump) + validate_buffer_pda(&pda, A_KNOWN_MINT.as_array(), bump) .expect("the known buffer PDA must be accepted"); } @@ -341,13 +337,8 @@ mod tests { fn rejects_an_invalid_address_for_a_known_mint() { let (_, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT); - let err = validate_buffer_pda( - &crate::ID, - &Pubkey::new_unique(), - A_KNOWN_MINT.as_array(), - bump, - ) - .expect_err("a non-canonical address must be rejected"); + let err = validate_buffer_pda(&Pubkey::new_unique(), A_KNOWN_MINT.as_array(), bump) + .expect_err("a non-canonical address must be rejected"); assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); } @@ -355,7 +346,7 @@ mod tests { fn ignores_the_bump_for_a_known_mint() { let (pda, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT); - validate_buffer_pda(&crate::ID, &pda, A_KNOWN_MINT.as_array(), bump ^ 1) + validate_buffer_pda(&pda, A_KNOWN_MINT.as_array(), bump ^ 1) .expect("the known buffer PDA must be accepted whatever the bump"); } diff --git a/programs/settlement/src/processor/begin_settle.rs b/programs/settlement/src/processor/begin_settle.rs index 861f7e92..6c250ebe 100644 --- a/programs/settlement/src/processor/begin_settle.rs +++ b/programs/settlement/src/processor/begin_settle.rs @@ -297,7 +297,7 @@ fn process_order( if Asset::is_native_sol(buy_mint) { validate_is_state_pda(push.source_buffer.as_array())?; } else { - validate_buffer_pda(program_id, push.source_buffer, buy_mint, push.bump)?; + validate_buffer_pda(push.source_buffer, buy_mint, push.bump)?; } // The sell token account must be the one named in the intent, owned by From 2049d6e6df197a1e85de210616e9a71084714ad8 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:22:26 +0900 Subject: [PATCH 18/20] re-add missing design file --- DESIGN.md | 389 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 00000000..375f2d10 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,389 @@ +# Solana program high-level specs + +There's a single dedicated settlement program, deployed once and used for executing all settlements. + +The settlement program stores funds through dedicated token accounts (_buffer accounts_), one per token. + +It uses a dedicated state account to: + +- Store the [authorities](#authorities) of the protocol. +- Store the accounts that can execute a settlement (the _solvers_). +- Act as a token delegate to manage user funds. + +Its state is stored in a PDA generated using a seed based on the cargo package version, like `["settlement v0.1"]`. + +Once the testing phase has ended, we will make the code at that account unchangeable. + +## State versioning + +Every PDA the program derives starts with the same prefix seed: the string `settlement`, then the current cargo package major and minor version prefixed by `v`, space-padded to a fixed width to prevent accidental prefix collisions. While we expect the ultimate settlement program to be immutable so this measure would normally not be needed, during development we want to avoid any possible confusion or losses that could occur as a result of stale data still existing within the settlement program. + +Bumping the minor version relocates the program's entire account storage at once — the state account, every buffer, and every order. + +The state PDA's address and bump are pinned at compile time against the declared program ID, so handlers compare the account against a constant instead of deriving it on-chain. As a safeguard, `Initialize` still derives it, and it rejects a state account that doesn't match the constant. + +A bump is not a migration. There are some other consequences that should be considered before the new program version is deployed: + +- **User delegations stop working.** Users delegate their token accounts to the state PDA (see [user delegation](#user-delegation-ie-approvals)). A bump moves that address, so every user has to delegate again before they can trade. +- **Whatever the buffers still hold is stranded.** A buffer's funds are only spendable by the state PDA that is its SPL authority. After a bump the program can no longer sign for the old state PDA, so it can never move those funds again. Buffers must be drained under the old program version *before* deploying a bump. + +## Program initialization + +The state PDA is created once, after deployment, by the `Initialize` instruction. + +## Authorities + +The program grant privileged roles to specific accounts (_authorities_). They are: + +- Manager: the account that can update the address of all other roles. +- Solver Authority: the account that can add and remove solvers. +- Reclaim Authority: the account authorized to close buffer accounts, reclaim their rent, and choose where that rent goes. +- Settlement-Owned-Order Authority: the account authorized to place arbitrary orders that sell the protocol's own buffer balances (for fee withdrawals). + +### Updating authorities + +Any authority that holds a role can transfer it to another account with the `TransferAuthority` instruction, which sets the role's holder to the new account in a single step. The manager can transfer any role. + +## Buffer accounts + +Buffer accounts are token accounts that hold funds on behalf of the settlement program through the state PDA. + +These token accounts are accessible to all solvers and effectively work like the current buffers. They are used to send out funds to the user and collect fees, which stay on the buffers after the settlement. This means that the current fee accounting and withdrawal mechanism would be based on balance changes (like on Ethereum). + +The buffer account for native SOL is the state PDA itself, using the funds on top of the necessary rent. + +Corresponding PDAs are generated using seed `[SETTLEMENT_SEED, token, "buffer"]`. + +A buffer (except for the state PDA) is closed by the `ReclaimBuffer` instruction, which only the [reclaim authority](#authorities) can call. + +Differences with Ethereum: + +- In a settlement, a solver can't access buffers directly but needs to invoke a dedicated instruction or use the funds in them to pay out the proceeds of an order. +- Traded funds aren't sent automatically to the buffers at the start of the trade. Solvers can specify arbitrary receiver accounts for the user's sell tokens, as well as send the trade proceeds from any arbitrary account. In practice, buffers may still be used by solvers for pooling funds before interactions for efficiency reasons. + +## Solver authentication + +Solver authentication is managed by the settlement program. + +There are two roles for authentication: + +- The _solvers_, accounts that can execute settlements and withdraw from the buffers. +- The _solver authority_, an account that can add and remove solvers, as well as transfer its own role to another account. + +The settlement program state PDA stores the state used for authentication. + +On settlement program deployment, the program state is initialized with fixed initial authorities, controlled by CoW Core as mandated by CoW DAO, and an empty list of solvers. + +Differences with Ethereum: + +- There's no dedicated authenticator contract, the settlement program performs the role of the authenticator contract. This also means that the authenticator isn't upgradable: there's no `owner`, only a set of authorities. + +Limitation: + +- Storing all solvers on the same account limits the amount of possible solver accounts to ~64k. + +## Fee withdrawals + +Fees accumulate in the buffer accounts after a settlement is concluded. + +Fees are withdrawn by placing an order, owned by the settlement state PDA, that sells tokens stored in a buffer. Order creation is gated by the dedicated [settlement-owned-order authority](#authorities). + +The order is placed through the `CreateSettlementOwnedOrder` instruction. The settlement-owned-order authority can specify arbitrary order parameters, as long as the owner is the state PDA and the order is marked as created on-chain. + +Differences with Ethereum: + +- Solvers can't access the content of the buffers directly anymore (though they can do so indirectly by creating a dedicated order just to sweep the buffer). +- `CreateSettlementOwnedOrder` has a dedicated authority rather than requiring a solver. +- Creating a fee-withdrawal order is done directly through a dedicated instruction, not indirectly as the result of a call from the settlement context. + +## User delegation (i.e., "approvals") + +As we'll see, trades work through signed intents. + +A user who wants to use CoW Protocol for a token needs to set their [delegate](https://solana.com/docs/tokens/basics/approve-delegate) for that token to the settlement state PDA. + +Similar to Ethereum's approvals, this needs to be done by each user once per token in order to trade. + +As long as the settlement program is immutable, there's no other way to access user funds except through the execution of an order. + +Differences with Ethereum: + +- There can only be a single delegate. This could create situations where the user delegates the settlement state PDA, creates an order, and then another dapp delegates a different program and the order can't be settled anymore. This is very different from approvals, where an approval for one dapp doesn't affect approvals for other dapps. +- There's no dedicated vault relayer, the user delegates their tokens to the settlement state PDA (_not_ the settlement program!). This is because "interactions" aren't executed by the settlement program but as dedicated instructions originating from the transaction signer. + +## Orders + +### Intents + +Users interact with the protocol by [signing](#authenticating-an-order) an order intent off-chain. + +An order intent is the following list of parameters: + +```rust +struct OrderIntent { + owner: Pubkey + + // Origin to pull funds for the order. Only SPL or Token-2022 token programs are supported. + sell: TokenAsset { + mint: Pubkey, + token_account: Pubkey, + } + + // Destination to push proceeds of the order. On top of the token support in sell, native SOL (lamports) may be purchased + buy: TokenAsset { + mint: Pubkey, + token_account: Pubkey, + } | NativeAsset { + account: Pubkey, + } + + // Amounts are interpreted as exact or maximum depending on kind. + sell_amount: u64 + buy_amount: u64 + + // Unix timestamp + valid_to: u32 + flags: Flags + + // Usual app data field, it isn't directly used in the program. + app_data: [u8; 32] +} + +struct Flags { + // Indicates the path by which the order was created. Important for reclaim. + created_on_chain: bool + // Either Buy or Sell + kind: OrderKind + partially_fillable: bool +} +``` + +The sell and buy tokens are effectively flattened down in wire format, and in the case that `buy` uses `NativeAsset`, +the `buy_mint` is set to the Solana system program. + +The fields grouped in `Flags` share a single byte in the encoded form, one bit +each, with the remaining bits reserved and required when decoding to be zero. + +Differences with Ethereum: + +- In Solana, the spender token account (and the owner) is part of the intent, while in Ethereum it is implied in the signature. +- The `receiver` field in Ethereum is captured by the field `buy_token_account`. +- Sell and buy amounts are 64 bits instead of Ethereum's 256. + +### Orders are accounts + +For processing an order in a settlement, the data of that order needs to be stored in a dedicated account. Storing this data is, in general, the responsibility of the solver who settles the order the first time, but anyone can do it if a user signed an order. + +Useful information can be recovered from the order PDA. Notably: + +```rust +// The PDA's canonical bump, stored so instructions don't have to carry it +bump: u8 +cancelled: bool +amount_withdrawn: u64 +amount_received: u64 +// The account that created this order, used for refunding rent +created_by: Pubkey +intent: OrderIntent +``` + +`amount_withdrawn` and `amount_received` are cumulative across all of the order's settlements. They cap how much of the order can still be traded, so it can't be filled beyond its sell and buy amounts. + +An order PDA can only exist and hold data if the order has been [authenticated](#authenticating-an-order). If this account exists and is not cancelled, filled, or expired, then the order can be traded. + +At the time of order creation, the executor can specify a different address as the `created_by` address. This allows rent to be reclaimed by a different account than the one that signed the instruction. + +Corresponding PDAs are generated using seed `[SETTLEMENT_SEED, hash(intent), "order"]`. + +The serialization of the parameters before hashing and the hashing function will be formally specified at a later point. + +### Identifying an order (order UID) + +We use the hash of the intent parameters to represent the order, `hash(parameters)`. This is the same hash used to generate the order PDA. + +Differences with Ethereum: + +- Solana's order UIDs are 32 bytes, compared to 56 bytes in Ethereum. This is because: + - The owner isn't added to the UID. The owner is already included in the parameters, unlike in Ethereum, thus the owner doesn't need to be appended for disambiguation. + - The expiration isn't added to the UID. In Ethereum it was only added because of state clearing, here it isn't needed. + +### Cancelling an order + +Cancelling an order is an operation executed by the user to make it impossible to trade that order in the protocol. + +Cancelling an order requires an on-chain operation. This operation can be authenticated in two ways: + +- Directly, through the `CancelOrder` instruction signed by the order owner account. +- By anyone through a signed intent, signing the following cancellation struct: + ```rust + struct CancelIntent { + intent: OrderIntent + } + ``` + +Creating the order in advance is _not_ needed: if the order wasn’t created before cancelling, the corresponding order PDA is created and then cancelled. + +Note that deleting the order PDA is _not_ enough to cancel an order for off-chain orders. In fact, if an order signature is available, the same order could always be created again until it expires. + +### Order clearing + +Allocating an order PDA requires paying rent. + +If the order is expired, anyone can close the order account through the `ReclaimOrder` instruction. On account closure, the rent is sent to the order's `created_by` account, i.e., the original creator of the order. + +If an order created on-chain (`created_on_chain`), it can safely be closed earlier. In this case, `ReclaimOrder` will additionally allow reclaiming of orders that are cancelled or completely filled. + +This is useful for solvers who need to allocate the order for executing it, but the allocation itself would be orders of magnitude more expensive than the compute cost for executing an instruction. This is particularly relevant to make small orders economically viable. + +## Authenticating an order + +We want to support two ways to authenticate that an order comes from a user: + +- [Off-chain] Through an [Ed25519 signature](#ed25519), encoded as Solana’s off-chain message. +- [On-chain] In an [instruction](#on-chain-order-creation) by the owner that directly creates the order PDA. + +### Encoding the signed data + +The data to sign is encoded using Solana’s [off-chain message signing standard](https://docs.anza.xyz/proposals/off-chain-message-signing). It’s similar to the ERC-712 standard to encode structured data, in that it guarantees that: + +- Users cannot sign Solana transactions instead of messages by accident. +- The signature cannot be used by other protocols that rely on the same signature encoding: there is strict namespacing of signatures, and each signed message is only valid for the intended purpose. + +Differences with Ethereum: + +- Off-chain message signing seems to be much less supported by wallets compared to ERC-712. For example, there doesn’t seem to be a Phantom wallet API to encode off-chain messages (see [documentation](https://docs.phantom.com/sdks/browser-sdk/sign-messages), Solana only has a simple `signMessage`). Further research is needed to determine wallet compatibility, but there’s reason to believe it’s very low and it will compromise user signing experience. +- Off-chain message signing doesn’t have a native representation of structured data. Only UTF8 and ASCII data can be signed, meaning that even if wallets were supporting this standard, many of them would just present, in the best case, a sequence of bytes. + +### Ed25519 + +Raw Ed25519 signatures are supported by all native Solana accounts. + +The data to be signed is encoded as an off-chain message and signed with raw Ed25519 signatures. + +Orders that are created by this path must specify the flag `created_on_chain = false`. + +Differences with Ethereum: + +- Unlike ECDSA signatures in Ethereum, the owner account address cannot be recovered from the Ed25519 signature. This means that the address needs to be included as part of the signed data. +- Signatures are 64 bytes (unlike EVM’s 65). + +### On-chain order creation + +Orders can be created by the owner by executing an instruction on-chain. + +The order owner executes the `CreateOrder` instruction. The settlement program checks that the order comes from the owner and [creates the order PDA](#orders-are-accounts). + +In this authentication flow, the user needs to pay for the rent in SOL necessary to create the PDA. Note that the rent may be significantly higher than the expected trading fee. The rent can be recovered by the user once the order has expired by [clearing the order](#order-clearing). + +Orders that are created by this path must specify the flag `created_on_chain = true`. + +This flow supports both standard ("on-curve") accounts and PDA signatures. + +This flow is the only one allowing on-chain programs to trade through the settlement program. + +Advantages: + +- Smart wallets can sign an order without needing an Ed25519 private key. + +Disadvantages: + +- Not intent-based, the user needs to sign and execute an instruction for every order. + +Differences with Ethereum: + +- The rent refund process isn’t part of Ethereum and adds extra costs for the user. +- Unlike Ethereum’s pre-signing, this flow isn’t considered a "signature scheme" but it’s a direct way to publish in a trusted way all order information on-chain. This is because, in Solana, it’s more complex and expensive to manage dedicated storage for pre-signatures than just creating the order on-chain. + +## Settlements + +### A settlement + +A settlement is composed of: + +- The orders to settle. +- The list of token transfers to perform from the orders’ sell token accounts and to the orders’ buy token account. +- A list of interactions to execute. + +Security guarantees: + +- User: + - Funds can only be taken in the context of a signed order. + - No more than the order’s sell amount can be taken from the user. + - The user receives at least the specified buy amount (proportional to how much of the sell amount was used in the settlement). +- Protocol: + - Only approved solvers can settle. + +Differences with Ethereum: + +- There’s no per-settlement list of prices. +- Fund transfers are explicit instead of being automatically done as part of the order inclusion. + +### The settlement transaction + +A settlement transaction is split into multiple instructions. All settlement operations occur between a `BeginSettle` and a `FinalizeSettle` instruction with the exception of arbitrary interactions, which can take place at any point of a transaction. Except for that, the order of instructions in the transaction is arbitrary. + +- `BeginSettle`: Pulls funds from each order’s sell token account to the solver-specified destination accounts, using the settlement state PDA’s token delegation. Validates each order's limit price and that its cumulative fill stays within the order's sell and buy amounts (fully filling a fill-or-kill order), and updates the order's `amount_withdrawn`/`amount_received`. Carries an explicit `finalize_ix_index` pointing to its paired `FinalizeSettle`. +- (arbitrary interactions): Any instruction from the solver. This could be a token transfer, an AMM swap, or anything else. +- `FinalizeSettle`: Pushes the proceeds of each order from the settlement’s buffer accounts to the order’s buy token account, using the settlement state PDA’s authority over the buffers. Carries an explicit `begin_ix_index` pointing to its paired `BeginSettle`. + +Additionally, `BeginSettle` includes the `auction_id` (an `i64`) as part of its instruction data. This value is unused by the program and only relied upon by the off-chain back-end services. + +Only a single settlement can be executed at a time. This means that there can’t be a `BeginSettle` instruction or a spurious `FinalizeSettle` instruction between a coupled pair of `Begin`/`FinalizeSettle`. + +Differences with Ethereum: + +- Interactions aren’t executed from the context of the settlement program but from the context of the solver as completely separate instructions. + - Notably: a solver doesn’t have to set approvals from the settlement contract to on-chain contracts. Once solver privileges are removed, there’s no way for old solvers to access the buffers anymore, unlike in the current Ethereum contract. +- Fund transfers are explicit instead of being automatically done as part of the order inclusion. These transfers aren’t automatically done to the buffers, they may go to different accounts. + +## Selling SOL (a.k.a. ETH flow) + +Selling SOL is supported through a separate program (called SOL flow). + +Selling native tokens requires taking control of the user funds and wrapping them. + +While the technical details are different, for the user this will be very similar to the ETH flow experience. + +Differences with Ethereum: + +- Minimum order size: needed to pay for rent by the user when the order is created. (But the rent will be used in the trade, there won’t be leftovers or need to recover this SOL by the user.) +- No unlimited order deadline. +- Different execution flow for solvers compared to EVM. + +### Creating a SOL sell order (user facing) + +The user interacts with the SolFlow program through the instruction `CreateSolOrder`. This instruction takes the traded amount of SOL and the data describing a [user intent](#intents) minus the owner (implicitly assumed to be the instruction signer), the sell token account, and the amount (assumed to be the SOL amount in the instruction). + +This will create an SPL token account for WSOL, owned by the SolFlow PDA, with seeds equal to (`intent`, `owner`), storing the intent data and the user’s lamports. We call this the _custodial_ account of the user for that intent. + +The token account will be created automatically with the delegate set to the settlement program. + +### Enabling a SOL sell order (solver facing) + +Any unprivileged participant can interact with the SolFlow program to have it create an Order in the Settlement program using the funds in the dedicated custodial account. We call that unprivileged participant the _enabler_. + +The enabler calls the `SetUpOrder` instruction passing on the user accounts (custodial and buy token accounts) and the user order intent. + +Then, the `SolFlow` processor creates an order on the `SettlementProgram` using a modified version of the original intent: + +- Order owner becomes the `SolFlow` PDA. +- Sell token becomes the _custodial_ token account. +- Order `created_by` field is set to enabler’s address. + +The `SetUpOrder` validates the given intent by successfully re-deriving the custodial account. + +### Recovering funds by the user + +Each intermediate step can be reverted by the original owner to recover the funds, as long as the order hasn’t been executed. It supports partial execution. + +### Comparison with direct wrapping and order creation + +Wrapping and creating an order is easier in Solana and everything can be executed in the same transaction. The delegation instruction can be bundled as well. + +The main reason to prefer the SOL flow described here is the handling of the rent amount. When wrapping and creating an order, the user needs to pay rent possibly for creating the order account. This is particularly notable for small orders: the order account would have to be refunded later to recover the user’s SOL. + +## Token 2022 + +The settlement program natively supports [Token-2022](https://www.solana-program.com/docs/token-2022) tokens. The settlement program does not explicitly block any +token extensions, but if the extension interferes with the settlement process such that it causes a failure (such as locked tokens), then any affected orders will +be unsettleable. From 3094f69dacfc473f6d9d9c001fee06be2a615cd4 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:24:11 +0900 Subject: [PATCH 19/20] fix lint --- interface/src/pda/buffer.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index 3e0201ec..74d2169c 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -229,7 +229,7 @@ mod tests { let (pda, bump) = find_buffer_pda(&crate::ID, &mint); let buffer = crate::instruction::fixtures::fake_account(pda); - validate_buffer_pda(&buffer.address(), mint.as_array(), bump) + validate_buffer_pda(buffer.address(), mint.as_array(), bump) .expect("the canonical buffer PDA must be accepted"); } @@ -240,7 +240,7 @@ mod tests { // An account sitting at some other address is not the buffer. let buffer = crate::instruction::fixtures::fake_account(Pubkey::new_unique()); - let err = validate_buffer_pda(&buffer.address(), mint.as_array(), bump) + let err = validate_buffer_pda(buffer.address(), mint.as_array(), bump) .expect_err("a non-canonical address must be rejected"); assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); } @@ -252,7 +252,7 @@ mod tests { // The address is canonical but the carried bump doesn't derive it. let buffer = crate::instruction::fixtures::fake_account(pda); - let err = validate_buffer_pda(&buffer.address(), mint.as_array(), bump ^ 1) + let err = validate_buffer_pda(buffer.address(), mint.as_array(), bump ^ 1) .expect_err("a wrong bump must be rejected"); assert_eq!(err, SettlementError::PushSourceNotBuffer.into()); } From 4b699d47be1c33c7528a6b0f89a64fd7ff8de141 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:35:50 +0900 Subject: [PATCH 20/20] better explain random source --- interface/src/pda/buffer.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index 74d2169c..945fd96f 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -125,7 +125,9 @@ const fn slot(mint: &[u8; 32], multiplier: u64) -> usize { /// Try the multipliers `SHA-256(0)`, `SHA-256(1)`, ... (each truncated to its /// first 8 bytes) until one sends each of `buffers` to a distinct [`slot`]. /// -/// SHA-256 is utilized to provide a pseudorandom source of multipliers, but any PRNG would work fine here. +/// SHA-256 is utilized to provide a pseudorandom source of multipliers. Normally +/// this shouldn't be necessary as the addresses themselves should already be effectively +/// random, but its possible some of the KNOWN_MINTS use vanity addresses. /// /// Panics (at compile time, for [`KNOWN_BUFFER_MULTIPLIER`]) if none does, /// which is certain if two mints share their leading 8 bytes.