diff --git a/bench-report.json b/bench-report.json index 0696ddbf..b68dbe98 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, @@ -78,17 +79,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": 4626, - "settle/pulls_from_multiple_orders": 13306, - "settle/pulls_funds_to_destination": 7242, - "settle/pulls_to_multiple_destinations": 8394, - "settle/pushes_a_single_order": 6082, - "settle/pushes_several_orders_from_different_buffers": 10985, - "settle/pushes_several_orders_from_one_buffer": 10985, - "settle/settles_a_single_order": 6100, - "settle/settles_multiple_orders": 15941, - "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7235, + "settle/finalizes_with_no_pushes": 1102, + "settle/happy_path_sell_tokens_for_native_sol": 4619, + "settle/pulls_from_multiple_orders": 13331, + "settle/pulls_funds_to_destination": 7254, + "settle/pulls_to_multiple_destinations": 8407, + "settle/pushes_a_single_order": 6095, + "settle/pushes_a_single_order_of_a_known_mint": 4584, + "settle/pushes_several_orders_from_different_buffers": 11010, + "settle/pushes_several_orders_from_one_buffer": 11015, + "settle/settles_a_single_order": 6113, + "settle/settles_multiple_orders": 15978, + "settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7242, "transfer_authority/manager_can_transfer_manager": 178, "transfer_authority/manager_can_transfer_reclaim_authority": 178, "transfer_authority/manager_can_transfer_settlement_owned_order_authority": 177, @@ -133,6 +135,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/src/pda/buffer.rs b/interface/src/pda/buffer.rs index 9302e57b..945fd96f 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -26,6 +26,9 @@ use solana_pubkey::Pubkey; 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"; @@ -35,7 +38,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,17 +53,151 @@ 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`], 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)] +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(), + ); + buffers[i] = KnownBuffer { mint, address }; + i += 1; + } + buffers +}; + +/// Bits of the hash that pick a slot: 512 slots keep collisions among 64 mints +/// 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`]. +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" + ); + let mut slots = [u8::MAX; 1 << SLOT_BITS]; + let mut i = 0; + while i < KNOWN_BUFFERS.len() { + slots[slot(&KNOWN_BUFFERS[i].mint, KNOWN_BUFFER_MULTIPLIER)] = i as u8; + i += 1; + } + slots +}; + +/// 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 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. 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. +const fn find_slot_multiplier(buffers: &[KnownBuffer]) -> u64 { + const ATTEMPTS: u32 = 100_000; + let mut attempt: u32 = 0; + while attempt < ATTEMPTS { + 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 + .checked_add(1) + .expect("attempt stays below ATTEMPTS"); + } + 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)]; + KNOWN_BUFFERS + .get(usize::from(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. 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( - program_id: &Address, buffer: &Address, 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(mint) { + Some(known) => buffer.as_array() == &known.address, + None => { + is_pda_with_signer_seeds(buffer, &crate::ID, buffer_pda_signer_seeds(mint, &[bump])) + } + }; + is_buffer .then_some(()) .ok_or(SettlementError::PushSourceNotBuffer.into()) } @@ -90,41 +227,131 @@ 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()); } + const A_KNOWN_MINT: Pubkey = Pubkey::from_str_const(KNOWN_MINTS[0]); + + #[test] + 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()); + } + } + + #[test] + fn every_known_mint_looks_up_its_own_buffer() { + for mint in known_mints::KNOWN_MINTS { + let mint = Pubkey::from_str_const(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()); + } + } + + #[test] + 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()); + } + + #[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 = *A_KNOWN_MINT.as_array(); + other[31] ^= 1; + let _ = find_slot_multiplier(&[ + KnownBuffer { + mint: *A_KNOWN_MINT.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, &A_KNOWN_MINT); + + validate_buffer_pda(&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, &A_KNOWN_MINT); + + 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()); + } + + #[test] + fn ignores_the_bump_for_a_known_mint() { + let (pda, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT); + + validate_buffer_pda(&pda, A_KNOWN_MINT.as_array(), bump ^ 1) + .expect("the known buffer PDA must be accepted whatever the bump"); + } + 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..d5f4498c --- /dev/null +++ b/interface/src/pda/buffer/known_mints.rs @@ -0,0 +1,67 @@ +/// The 63 verified tokens sampled by a snapshot of Jupiter's trading API +/// Sorted by address for easier diffing +pub const KNOWN_MINTS: [&str; 63] = [ + "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", // JLP + "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", // PYUSD + "2u1tszSeqZ3qBWF3uNGPFc8TzMk2tdiwknnRMWGWjGWH", // USDG + "2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv", // PENGU + "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", // WBTC + "3ZLekZYq2qkZiSpnSvabjit34tUkjSwD1JFuW9as9wBG", // wNEAR + "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R", // RAY + "4sWNB8zGWHkh6UnmwiEtzNxL4XrN7uK9tosbESbJFfVs", // xSOL + "5Y8NV33Vv7WbnLfq3zBcKSdYPrk7g2KoiQoe7M2tcxp5", // ONyc + "5YMkXAYccHSGnHn9nob9xEvv6Pvka9DZWH7nTbotTu9E", // hyUSD + "6GmAFSYs4gk3FDao5FzzySQpPZaWsa4rUJHacpMpUNgx", // STONK + "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN", // TRUMP + "7GxATsNMnaC88vdwd2t3mwrFuQwwGvmYPrUQ4D6FotXk", // JUICED + "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", // ETH + "98sMhvDwXj1RQi5c5Mndm3vPe9cBqPrbLaufMXFNMh5g", // HYPE + "9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump", // Fartcoin + "9BEcn9aPEmhSPbPQeFGjidRiEKki46fVQDyPpSQXPA2D", // jlUSDC + "9cRCn9rGT8V2imeM2BaKs13yhMEais3ruM3rPvTGpump", // ANSEM + "9fvHrYNw1A8Evpcj7X2yy4k4fT7nNHcA9L6UsamNHAif", // jlUSDG + "A7bdiYdS5GjqGFtxf17ppRHtDKPkkRqbKtR27dxvQXaS", // ZEC + "Ai66LHZG9MCzg1WKdawwqduVAXpNDUuV8M3uyq5ppump", // CATE + "BPxxfRCXkUVhig4HS1Lh7kZqV6SPJhzfEk4x6fVBjPCy", // BP + "CARDSccUMFKoPRZxt5vt3ksUbxEFEcnZ3H2pd3dKxYjp", // CARDS + "CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH", // CASH + "CWZ6BsdnjkDVTGkmL6bGbJXXig6ceef12KvyGQW14cMt", // ANTFUN + "DEkqHyPN7GMRJ5cArtQFAWefqbZb33Hyf6s5iCwjEonT", // USDe + "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 + "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 + "XsueG8BtpquVJX9LVLLEGuViXUungE6WmK5YZ3p3bd1", // CRCLx + "Xsv9hRk1z5ystj9MhnA7Lq4vjSsLwzL2nxrwmwtD3re", // GLDx + "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij", // cbBTC + "oPAiAikWTaFj9RYoRFD35ccfwhnMcB3ThgBZRHSkjTZ", // tOpenAI + "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", // PUMP +]; diff --git a/programs/settlement/src/processor/begin_settle.rs b/programs/settlement/src/processor/begin_settle.rs index efdbce3a..d70856d8 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 diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index 7733c524..616c3d14 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -58,6 +58,20 @@ 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(), + 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..c6fc4ccc 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -22,14 +22,14 @@ 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, }; 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,33 @@ 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(); + 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) + .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();