diff --git a/Cargo.lock b/Cargo.lock index 1b0d229..7428009 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -923,7 +923,7 @@ dependencies = [ "solana-sdk-ids", "solana-sha256-hasher", "solana-system-interface 3.2.0", - "spl-token-interface 3.0.0", + "spl-token-2022-interface 3.1.1", ] [[package]] @@ -2086,6 +2086,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsecp256k1" version = "0.7.2" @@ -2459,6 +2465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2616,14 +2623,29 @@ dependencies = [ [[package]] name = "pinocchio-token" -version = "0.6.0" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217e3259f93a1520e4692b18653dad4d29af54ffc8b3a09819808be85dada074" +dependencies = [ + "solana-account-view", + "solana-address 2.6.1", + "solana-instruction-view", + "solana-program-error", +] + +[[package]] +name = "pinocchio-token-2022" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825f59c8348e5c2d3fd56432ef927f5819542b3b05fae4f5b6869801113e775e" +checksum = "3a3c164973916f044bebff3c8cb4467088bafb13cbdb818522b9ca8782940d06" dependencies = [ + "pinocchio-token", "solana-account-view", "solana-address 2.6.1", "solana-instruction-view", + "solana-nullable", "solana-program-error", + "solana-zero-copy", ] [[package]] @@ -3308,6 +3330,7 @@ dependencies = [ "pinocchio", "pinocchio-system", "pinocchio-token", + "pinocchio-token-2022", "proptest", "serde_json", "solana-address-lookup-table-interface", @@ -3318,6 +3341,7 @@ dependencies = [ "solana-sdk", "solana-sha256-hasher", "solana-system-interface 3.2.0", + "spl-token-2022-interface 3.1.1", "syn 2.0.119", ] @@ -3486,10 +3510,10 @@ dependencies = [ "solana-sysvar 4.1.0", "solana-vote-interface", "spl-generic-token", - "spl-token-2022-interface", + "spl-token-2022-interface 2.1.0", "spl-token-group-interface", "spl-token-interface 2.0.0", - "spl-token-metadata-interface", + "spl-token-metadata-interface 0.8.0", "thiserror 2.0.19", "zstd", ] @@ -4321,6 +4345,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "889194d8c5faec648f2f6fadddb60566249921ebb074e2707e7095458d5864e2" dependencies = [ + "borsh", "bytemuck", ] @@ -5364,6 +5389,22 @@ dependencies = [ "bytemuck_derive", ] +[[package]] +name = "solana-zk-elgamal-proof-interface" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8da7f01db2148a1dc16261ff1dc6f3930a1e255a33cece4f1b56658694f27f7" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "num-derive", + "num-traits", + "solana-address 2.6.1", + "solana-instruction", + "solana-sdk-ids", + "solana-zk-sdk-pod", +] + [[package]] name = "solana-zk-elgamal-proof-program" version = "4.1.2" @@ -5449,6 +5490,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "solana-zk-sdk-pod" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a800583b7a4cea3e851686af162cc6e4712eef97fa91dfa9aca2459b95c84777" +dependencies = [ + "base64", + "bytemuck", + "bytemuck_derive", + "solana-nullable", + "thiserror 2.0.19", +] + [[package]] name = "solana-zk-token-proof-program" version = "4.1.2" @@ -5592,10 +5646,40 @@ dependencies = [ "solana-sdk-ids", "solana-zk-sdk 4.0.0", "spl-pod", - "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-extraction 0.5.1", "spl-token-confidential-transfer-proof-generation", "spl-token-group-interface", - "spl-token-metadata-interface", + "spl-token-metadata-interface 0.8.0", + "spl-type-length-value", + "thiserror 2.0.19", +] + +[[package]] +name = "spl-token-2022-interface" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "821d96d034ea31c4965d182c742153c491ae0abee531331b55771086c5030d86" +dependencies = [ + "arrayref", + "bytemuck", + "getrandom 0.2.17", + "num-derive", + "num-traits", + "num_enum", + "solana-account-info", + "solana-address 2.6.1", + "solana-instruction", + "solana-nullable", + "solana-program-error", + "solana-program-option", + "solana-program-pack", + "solana-sdk-ids", + "solana-zero-copy", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", + "spl-token-confidential-transfer-proof-extraction 0.6.1", + "spl-token-group-interface", + "spl-token-metadata-interface 1.0.1", "spl-type-length-value", "thiserror 2.0.19", ] @@ -5620,6 +5704,26 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a845ec724e807643a04f1d43439ee3571dd913848112ac76aa90b850eaf7c" +dependencies = [ + "bytemuck", + "solana-account-info", + "solana-address 2.6.1", + "solana-curve25519 4.0.1", + "solana-instruction", + "solana-instructions-sysvar 3.0.1", + "solana-msg", + "solana-program-error", + "solana-sdk-ids", + "solana-zk-elgamal-proof-interface", + "solana-zk-sdk-pod", + "thiserror 2.0.19", +] + [[package]] name = "spl-token-confidential-transfer-proof-generation" version = "0.5.1" @@ -5709,6 +5813,26 @@ dependencies = [ "thiserror 2.0.19", ] +[[package]] +name = "spl-token-metadata-interface" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3d96f175e7022ff200464dfa75a3708a4e9b70c83c4ecd04fe52ee479f4fef" +dependencies = [ + "borsh", + "num-derive", + "num-traits", + "num_enum", + "solana-address 2.6.1", + "solana-borsh", + "solana-instruction", + "solana-nullable", + "solana-program-error", + "spl-discriminator", + "spl-type-length-value", + "thiserror 2.0.19", +] + [[package]] name = "spl-type-length-value" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index d2e2dbb..e691330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,8 @@ litesvm-token = "0.15" num_enum = "0.7" pinocchio = "0.11" pinocchio-system = "0.6" -pinocchio-token = "0.6" +pinocchio-token = "0.7" +pinocchio-token-2022 = "0.4" proptest = "1" serde_json = { version = "1", features = ["preserve_order"] } cow-settlement-client = { path = "client", version = "0.3.0" } @@ -58,6 +59,7 @@ solana-sha256-hasher = { version = "3", features = ["sha2"] } solana-system-interface = "3" spl-associated-token-account-interface = "2" spl-token = "9" +spl-token-2022-interface = "3" spl-token-interface = "3" syn = { version = "2", features = ["full"] } diff --git a/DESIGN.md b/DESIGN.md index bd79007..a3283ae 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -363,4 +363,6 @@ The main reason to prefer the SOL flow described here is the handling of the ren ## Token 2022 -The settlement program will natively support [Token-2022](https://www.solana-program.com/docs/token-2022) tokens. All operations available to standard tokens will be usable for tokens based on this standard, and no major front-end or back-end changes are expected in order to support the majority of tokens based on this standard. +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/bench-report.json b/bench-report.json index 6dc671d..192c144 100644 --- a/bench-report.json +++ b/bench-report.json @@ -34,34 +34,32 @@ "compute_units": { "add_solver/add_with_many_existing_solvers": 5074, "add_solver/adds_a_solver": 4622, - "create_buffers/happy_path_creates_initialized_buffer_token_account": 7347, - "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17247, - "create_buffers/max_buffers_in_one_instruction": 169571, + "create_buffers/happy_path_creates_initialized_buffer_token_account": 7376, + "create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 17290, + "create_buffers/max_buffers_in_one_instruction": 169803, "create_order/happy_path_creates_order_pda_with_expected_body": 4985, - "initialize/happy_path_initializes_state_pda_with_expected_data": 4530, - "reclaim_buffer/funded_buffer_is_skipped": 4835, - "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 5982, - "reclaim_buffer/max_buffers_in_one_instruction": 124622, - "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7581, + "initialize/happy_path_initializes_state_pda_with_expected_data": 4529, + "reclaim_buffer/funded_buffer_is_skipped": 4859, + "reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 6009, + "reclaim_buffer/max_buffers_in_one_instruction": 124823, + "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 7611, "reclaim_order/happy_path_expired_returns_lamports_and_closes_pda": 2202, - "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2071, + "reclaim_order/happy_path_on_chain_order_cancelled_is_reclaimable_before_expiry": 2072, "reclaim_order/happy_path_on_chain_order_fully_filled_is_reclaimable_before_expiry": 2079, "reclaim_order/off_chain_order_is_reclaimable_only_once_expired": null, "reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null, - "remove_solver/remove_with_many_existing_solvers": 3757, - "remove_solver/removes_a_solver": 3492, - "settle/finalizes_with_no_pushes": 7154, - "settle/pulls_from_multiple_orders": 20059, - "settle/pulls_funds_to_destination": 13640, - "settle/pulls_to_multiple_destinations": 14781, - "settle/pushes_a_single_order": 12495, - "settle/pushes_several_orders_from_different_buffers": 17767, - "settle/pushes_several_orders_from_one_buffer": 17766, - "settle/settles_a_single_order": 12513, - "settle/settles_multiple_orders": 23086, - "transfer_authority/manager_can_transfer_manager": 3173, - "transfer_authority/manager_can_transfer_reclaim_authority": 3175, - "transfer_authority/reclaim_authority_can_transfer_itself": 3179 + "settle/finalizes_with_no_pushes": 7171, + "settle/pulls_from_multiple_orders": 20076, + "settle/pulls_funds_to_destination": 13657, + "settle/pulls_to_multiple_destinations": 14798, + "settle/pushes_a_single_order": 12512, + "settle/pushes_several_orders_from_different_buffers": 17784, + "settle/pushes_several_orders_from_one_buffer": 17783, + "settle/settles_a_single_order": 12530, + "settle/settles_multiple_orders": 23103, + "transfer_authority/manager_can_transfer_manager": 3174, + "transfer_authority/manager_can_transfer_reclaim_authority": 3176, + "transfer_authority/reclaim_authority_can_transfer_itself": 3180 }, "transaction_bytes": { "add_solver/add_with_many_existing_solvers": 366, diff --git a/client/src/instructions.rs b/client/src/instructions.rs index 207016e..eaec239 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -8,6 +8,7 @@ use cow_settlement_interface::{ data::intent::{EncodedOrderIntent, OrderIntent}, pda::{buffer::find_buffer_pda, order::find_order_pda, state::find_state_pda}, + token_program::TokenProgram, Instruction, Pubkey, Role, }; @@ -148,6 +149,7 @@ impl From> for Instruction { pub struct CreateBuffers<'a> { pub program_id: Pubkey, pub payer: Pubkey, + pub token_program: TokenProgram, pub mints: &'a [Pubkey], } @@ -161,6 +163,7 @@ impl From> for Instruction { cow_settlement_interface::instruction::create_buffer::CreateBuffers { program_id: builder.program_id, payer: builder.payer, + token_program: builder.token_program.address(), buffers: &buffers, } .into() @@ -199,6 +202,7 @@ pub struct ReclaimBuffer<'a> { pub program_id: Pubkey, pub reclaim_authority: Pubkey, pub reclaim_recipient: Pubkey, + pub token_program: TokenProgram, pub mints: &'a [Pubkey], } @@ -218,6 +222,7 @@ impl From> for Instruction { state_pda, reclaim_authority: builder.reclaim_authority, reclaim_recipient: builder.reclaim_recipient, + token_program: builder.token_program.address(), buffers: &buffers, } .into() diff --git a/client/src/parse.rs b/client/src/parse.rs index 7f0c235..6d5eeaa 100644 --- a/client/src/parse.rs +++ b/client/src/parse.rs @@ -88,6 +88,7 @@ mod tests { fixtures::fake_account_from_array, reclaim_buffer::ReclaimBuffer, reclaim_order::ReclaimOrder, transfer_authority::TransferAuthority, }, + token_program::TokenProgram, Instruction, Role, }; @@ -115,6 +116,7 @@ mod tests { SettlementInstruction::CreateBuffer => CreateBuffers { program_id, payer, + token_program: TokenProgram::SplToken, mints: &[pubkey_from_seed("mint")], } .into(), @@ -146,6 +148,7 @@ mod tests { state_pda: pubkey_from_seed("state pda"), reclaim_authority: payer, reclaim_recipient: payer, + token_program: pubkey_from_seed("token program"), buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"))], } .into(), diff --git a/interface/Cargo.toml b/interface/Cargo.toml index 348ef7b..da903ed 100644 --- a/interface/Cargo.toml +++ b/interface/Cargo.toml @@ -30,7 +30,7 @@ solana-pubkey = { workspace = true, features = ["curve25519"] } solana-sdk-ids.workspace = true solana-sha256-hasher.workspace = true solana-system-interface.workspace = true -spl-token-interface.workspace = true +spl-token-2022-interface.workspace = true [dev-dependencies] hex-literal.workspace = true diff --git a/interface/src/instruction/create_buffer.rs b/interface/src/instruction/create_buffer.rs index 0d3b25e..976d94d 100644 --- a/interface/src/instruction/create_buffer.rs +++ b/interface/src/instruction/create_buffer.rs @@ -14,10 +14,6 @@ pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID; use super::InstructionInputParsing; use crate::SettlementInstruction; -/// The SPL Token program. Buffers are created as token accounts owned by this -/// program. -pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; - /// Builder for a `CreateBuffer` instruction that creates one buffer per /// `(buffer_pda, mint)` pair in `buffers`. /// @@ -30,6 +26,11 @@ pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; /// unchanged and the instruction still succeeds, so two parties racing to /// create the same buffer both succeed. /// +/// The token_program supplied to this instruction must be the owner of all mints +/// supplied. Only one token program can be supplied to this instruction at a time. +/// If mints from two separate token programs are required, the client needs to +/// divide it into separate instructions. +/// /// Wire format: `[discriminator=4]`, 1 byte. The tokens are implied by the /// `mint` accounts, so no further data is needed. /// Required accounts: @@ -40,6 +41,10 @@ pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; pub struct CreateBuffers<'a> { pub program_id: Pubkey, pub payer: Pubkey, + /// The token program that will own every buffer this instruction creates. + /// Must be the address of a [`crate::token_program::TokenProgram`], and + /// must be the program owning every `mint` in `buffers`. + pub token_program: Pubkey, pub buffers: &'a [(Pubkey, Pubkey)], } @@ -48,7 +53,7 @@ impl From> for Instruction { let mut accounts = vec![ AccountMeta::new(builder.payer, true), AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(builder.token_program, false), ]; for (buffer_pda, mint) in builder.buffers { accounts.push(AccountMeta::new(*buffer_pda, false)); @@ -136,6 +141,7 @@ pub mod fixtures { Instruction::from(CreateBuffers { program_id: zero, payer: zero, + token_program: zero, buffers: &[(zero, zero)], }) .data @@ -146,27 +152,26 @@ pub mod fixtures { mod tests { use super::fixtures::{create_buffer_data, NUM_SHARED_ACCOUNTS}; use super::*; - use crate::instruction::fixtures::{ - fake_account, fake_account_from_array, fake_sequential_accounts, - }; + use crate::fixtures::pubkey_from_seed; + use crate::instruction::fixtures::{fake_account, fake_sequential_accounts}; use crate::instruction::tests::{ assert_readonly_nonsigner, assert_writable_nonsigner, assert_writable_signer, }; use solana_account_view::AccountView; - use solana_address::Address; #[test] fn create_buffer_input_parses_valid_input() { - let program_id: Address = Address::new_from_array([1; 32]); - let payer: Address = Address::new_from_array([2; 32]); - let system_program = fake_account_from_array([4; 32]); - let token_program = Address::new_from_array([3; 32]); - let buffer_pda = Address::new_from_array([5; 32]); - let mint = Address::new_from_array([6; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let system_program = fake_account(pubkey_from_seed("system program")); + let buffer_pda = pubkey_from_seed("buffer pda"); + let mint = pubkey_from_seed("mint"); let data = Instruction::from(CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_pda, mint)], }) .data; @@ -190,23 +195,24 @@ mod tests { #[test] fn create_buffer_input_parses_multiple_buffers() { - let program_id = Address::new_from_array([1; 32]); - let payer = Address::new_from_array([2; 32]); - let token_program = Address::new_from_array([3; 32]); - let buffer_a = Address::new_from_array([5; 32]); - let mint_a = Address::new_from_array([6; 32]); - let buffer_b = Address::new_from_array([7; 32]); - let mint_b = Address::new_from_array([8; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let buffer_a = pubkey_from_seed("buffer a"); + let mint_a = pubkey_from_seed("mint a"); + let buffer_b = pubkey_from_seed("buffer b"); + let mint_b = pubkey_from_seed("mint b"); let data = Instruction::from(CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)], }) .data; let accounts = [ fake_account(payer), - fake_account_from_array([4; 32]), + fake_account(pubkey_from_seed("system program")), fake_account(token_program), fake_account(buffer_a), fake_account(mint_a), @@ -274,13 +280,15 @@ mod tests { #[test] fn instruction_data_has_expected_layout() { - let program_id = Pubkey::new_from_array([1; 32]); - let payer = Pubkey::new_from_array([2; 32]); - let buffer_pda = Pubkey::new_from_array([3; 32]); - let mint = Pubkey::new_from_array([4; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let buffer_pda = pubkey_from_seed("buffer pda"); + let mint = pubkey_from_seed("mint"); let Instruction { data, .. } = CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_pda, mint)], } .into(); @@ -292,13 +300,15 @@ mod tests { #[test] fn single_buffer_has_expected_accounts() { - let program_id = Pubkey::new_from_array([1; 32]); - let payer = Pubkey::new_from_array([2; 32]); - let buffer_pda = Pubkey::new_from_array([3; 32]); - let mint = Pubkey::new_from_array([4; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let buffer_pda = pubkey_from_seed("buffer pda"); + let mint = pubkey_from_seed("mint"); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_pda, mint)], } .into(); @@ -309,22 +319,24 @@ mod tests { // untouched. assert_writable_signer(&accounts[0], payer); assert_readonly_nonsigner(&accounts[1], SYSTEM_PROGRAM_ID); - assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID); + assert_readonly_nonsigner(&accounts[2], token_program); assert_writable_nonsigner(&accounts[3], buffer_pda); assert_readonly_nonsigner(&accounts[4], mint); } #[test] fn multiple_buffers_append_pairs_after_shared_accounts() { - let program_id = Pubkey::new_from_array([1; 32]); - let payer = Pubkey::new_from_array([2; 32]); - let buffer_a = Pubkey::new_from_array([3; 32]); - let mint_a = Pubkey::new_from_array([4; 32]); - let buffer_b = Pubkey::new_from_array([5; 32]); - let mint_b = Pubkey::new_from_array([6; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); + let buffer_a = pubkey_from_seed("buffer a"); + let mint_a = pubkey_from_seed("mint a"); + let buffer_b = pubkey_from_seed("buffer b"); + let mint_b = pubkey_from_seed("mint b"); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, + token_program, buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)], } .into(); @@ -339,11 +351,13 @@ mod tests { #[test] fn empty_buffers_has_only_shared_accounts() { - let program_id = Pubkey::new_from_array([1; 32]); - let payer = Pubkey::new_from_array([2; 32]); + let program_id = pubkey_from_seed("program id"); + let payer = pubkey_from_seed("payer"); + let token_program = pubkey_from_seed("token program"); let Instruction { accounts, .. } = CreateBuffers { program_id, payer, + token_program, buffers: &[], } .into(); diff --git a/interface/src/instruction/reclaim_buffer.rs b/interface/src/instruction/reclaim_buffer.rs index 5a8559e..ad400f4 100644 --- a/interface/src/instruction/reclaim_buffer.rs +++ b/interface/src/instruction/reclaim_buffer.rs @@ -7,6 +7,11 @@ //! //! A buffer that still holds tokens is skipped, not closed. //! +//! The token_program supplied to this instruction must be the owner of all mints +//! supplied. Only one token program can be supplied to this instruction at a time. +//! If mints from two separate token programs are required, the client needs to +//! divide it into separate instructions. +//! //! Wire format: `[discriminator=6]`, 1 byte. //! Required accounts: //! `[state_pda (R), reclaim_authority (R,S), reclaim_recipient (W), token_program (R), (buffer_pda (W), mint (R))...]`. @@ -16,7 +21,6 @@ use solana_program_error::ProgramError; use solana_pubkey::Pubkey; use super::InstructionInputParsing; -pub use crate::instruction::create_buffer::SPL_TOKEN_PROGRAM_ID; use crate::SettlementInstruction; /// Builder for a `ReclaimBuffer` instruction that closes one buffer per @@ -37,6 +41,9 @@ pub struct ReclaimBuffer<'a> { pub state_pda: Pubkey, pub reclaim_authority: Pubkey, pub reclaim_recipient: Pubkey, + /// The token program owning every buffer this instruction closes. Must be + /// the address of a [`crate::token_program::TokenProgram`]. + pub token_program: Pubkey, pub buffers: &'a [(Pubkey, Pubkey)], } @@ -46,7 +53,7 @@ impl From> for Instruction { AccountMeta::new_readonly(builder.state_pda, false), AccountMeta::new_readonly(builder.reclaim_authority, true), AccountMeta::new(builder.reclaim_recipient, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(builder.token_program, false), ]; for (buffer_pda, mint) in builder.buffers { accounts.push(AccountMeta::new(*buffer_pda, false)); @@ -126,6 +133,7 @@ pub mod fixtures { state_pda: zero, reclaim_authority: zero, reclaim_recipient: zero, + token_program: zero, buffers: &[(zero, zero)], }) .data @@ -148,6 +156,7 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let buffer_pda = pubkey_from_seed("buffer pda"); let mint = pubkey_from_seed("mint"); @@ -156,6 +165,7 @@ mod tests { state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[(buffer_pda, mint)], }) .data; @@ -201,6 +211,7 @@ mod tests { let data = Instruction::from(ReclaimBuffer { program_id, state_pda, + token_program, reclaim_authority, reclaim_recipient, buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)], @@ -282,6 +293,7 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let buffer_pda = pubkey_from_seed("buffer pda"); let mint = pubkey_from_seed("mint"); let Instruction { data, .. } = ReclaimBuffer { @@ -289,6 +301,7 @@ mod tests { state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[(buffer_pda, mint)], } .into(); @@ -304,6 +317,7 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let buffer_pda = pubkey_from_seed("buffer pda"); let mint = pubkey_from_seed("mint"); let Instruction { accounts, .. } = ReclaimBuffer { @@ -311,6 +325,7 @@ mod tests { state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[(buffer_pda, mint)], } .into(); @@ -319,7 +334,7 @@ mod tests { assert_readonly_nonsigner(&accounts[0], state_pda); assert_readonly_signer(&accounts[1], reclaim_authority); assert_writable_nonsigner(&accounts[2], reclaim_recipient); - assert_readonly_nonsigner(&accounts[3], SPL_TOKEN_PROGRAM_ID); + assert_readonly_nonsigner(&accounts[3], token_program); assert_writable_nonsigner(&accounts[4], buffer_pda); assert_readonly_nonsigner(&accounts[5], mint); } @@ -332,6 +347,7 @@ mod tests { state_pda: pubkey_from_seed("state pda"), reclaim_authority, reclaim_recipient: reclaim_authority, + token_program: pubkey_from_seed("token program"), buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"))], } .into(); @@ -346,6 +362,7 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let buffer_a = pubkey_from_seed("buffer a"); let mint_a = pubkey_from_seed("mint a"); let buffer_b = pubkey_from_seed("buffer b"); @@ -355,6 +372,7 @@ mod tests { state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)], } .into(); @@ -373,11 +391,13 @@ mod tests { let state_pda = pubkey_from_seed("state pda"); let reclaim_authority = pubkey_from_seed("reclaim authority"); let reclaim_recipient = pubkey_from_seed("reclaim recipient"); + let token_program = pubkey_from_seed("token program"); let Instruction { accounts, .. } = ReclaimBuffer { program_id, state_pda, reclaim_authority, reclaim_recipient, + token_program, buffers: &[], } .into(); diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index bc6d37b..8ba0859 100644 --- a/interface/src/instruction/settle/mod.rs +++ b/interface/src/instruction/settle/mod.rs @@ -1,10 +1,12 @@ //! `BeginSettle`/`FinalizeSettle` instruction tools, the instructions-sysvar //! account ID they all reference, and the off-chain instruction builders. +use crate::{token_program::TokenProgram, Pubkey}; use solana_program_error::ProgramError; +/// The legacy SPL Token program, which the builders below target by default. +pub const SPL_TOKEN_PROGRAM_ID: Pubkey = TokenProgram::SplToken.address(); pub use solana_sdk_ids::sysvar::instructions::ID as INSTRUCTIONS_SYSVAR_ID; -pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID; mod begin; mod finalize; diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 82398ba..efd2cf0 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -9,6 +9,7 @@ solana_pubkey::declare_id!("FYp8R5K4B3B1Kfr7QuWzMz4TwoT7wptjYtxgCrY5sRXb"); pub mod data; pub mod instruction; pub mod pda; +pub mod token_program; #[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] #[repr(u8)] @@ -27,7 +28,7 @@ pub enum SettlementInstruction { CreateOrder = 2, /// Creates the singleton settlement state PDA. Succeeds only once. Initialize = 3, - /// Creates one or more per-token buffer PDAs (SPL token accounts) in a + /// Creates one or more per-token buffer PDAs (token accounts) in a /// single instruction. /// /// Each buffer_pda_i must be the canonical PDA for seeds @@ -254,6 +255,10 @@ pub enum SettlementError { /// A created order's intent isn't set with the `created_on_chain` flag /// corresponding to the behavior of the invoked order creation instruction. OrderCreatedOnChainMismatch = 39, + /// `CreateBuffer` asked the token program how long a token account for a + /// mint has to be and couldn't read the answer, so it can't size the + /// buffer. + BufferSizeUnavailable = 40, } impl From for u32 { diff --git a/interface/src/pda/buffer.rs b/interface/src/pda/buffer.rs index e3a25d1..98a22ff 100644 --- a/interface/src/pda/buffer.rs +++ b/interface/src/pda/buffer.rs @@ -16,8 +16,8 @@ //! contents are stranded. //! //! Unlike the order PDA, which stores its own bump (see -//! [`crate::data::order::OrderAccount`]), a buffer is a fixed-size SPL token -//! account with no room for one. +//! [`crate::data::order::OrderAccount`]), a buffer's layout belongs entirely to +//! the token program, leaving no room for one. use solana_address::Address; use solana_program_error::ProgramError; diff --git a/interface/src/token_program.rs b/interface/src/token_program.rs new file mode 100644 index 0000000..c84b298 --- /dev/null +++ b/interface/src/token_program.rs @@ -0,0 +1,72 @@ +//! Utilities related to the token programs supported by the settlement program. + +use crate::Pubkey; +use solana_program_error::ProgramError; + +/// A token program a token-moving instruction accepts. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TokenProgram { + /// The legacy SPL Token program. + SplToken, + /// The SPL Token-2022 program. + Token2022, +} + +impl TokenProgram { + /// Every supported token program, in no particular order. The single list + /// [`TryFrom`] resolves addresses against. + pub const ALL: [Self; 2] = [Self::SplToken, Self::Token2022]; + + /// The address the program is deployed at. + pub const fn address(self) -> Pubkey { + match self { + Self::SplToken => spl_token_2022_interface::inline_spl_token::ID, + Self::Token2022 => spl_token_2022_interface::ID, + } + } +} + +impl TryFrom<&Pubkey> for TokenProgram { + type Error = ProgramError; + + /// Resolves a program address to the token program it identifies, + /// rejecting any address that isn't a supported token program. + fn try_from(address: &Pubkey) -> Result { + Self::ALL + .into_iter() + .find(|program| program.address() == *address) + .ok_or(ProgramError::IncorrectProgramId) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fixtures::pubkey_from_seed; + + /// Adding a variant makes this match non-exhaustive, the compile-time reminder + /// to add it to [`TokenProgram::ALL`] as well. + const _: () = match TokenProgram::SplToken { + TokenProgram::SplToken | TokenProgram::Token2022 => (), + }; + + #[test] + fn every_program_resolves_from_its_own_address() { + for program in TokenProgram::ALL { + let address = program.address(); + assert_eq!( + TokenProgram::try_from(&address), + Ok(program), + "{program:?} should resolve from {address}", + ); + } + } + + #[test] + fn unrelated_program_cannot_be_resolved_as_token_program() { + assert_eq!( + TokenProgram::try_from(&pubkey_from_seed("not a token program")), + Err(ProgramError::IncorrectProgramId), + ); + } +} diff --git a/programs/settlement/Cargo.toml b/programs/settlement/Cargo.toml index 20afb9e..9f9e01d 100644 --- a/programs/settlement/Cargo.toml +++ b/programs/settlement/Cargo.toml @@ -21,6 +21,7 @@ crate-type = ["cdylib", "lib"] pinocchio = { workspace = true, features = ["cpi", "account-resize"] } pinocchio-system.workspace = true pinocchio-token.workspace = true +pinocchio-token-2022.workspace = true cow-settlement-interface.workspace = true solana-instruction = { workspace = true, features = ["syscalls"] } @@ -41,6 +42,7 @@ solana-program-pack.workspace = true solana-sdk.workspace = true solana-sha256-hasher.workspace = true solana-system-interface.workspace = true +spl-token-2022-interface.workspace = true syn.workspace = true [lints] diff --git a/programs/settlement/idl/cow_settlement.json b/programs/settlement/idl/cow_settlement.json index bdd9b90..48c19b6 100644 --- a/programs/settlement/idl/cow_settlement.json +++ b/programs/settlement/idl/cow_settlement.json @@ -85,7 +85,8 @@ { "name": "create_buffer", "docs": [ - "Creates one or more per-token buffer PDAs (SPL token accounts) in a single instruction.", + "Creates one or more per-token buffer PDAs (token accounts) in a single instruction.", + "Every buffer created by one instruction is owned by the single token_program the instruction is handed, so mints spread across both supported token programs need one instruction each.", "IDL LIMITATION: the real instruction accepts an unbounded number of (buffer_pda, mint) pairs as remaining accounts, one pair per buffer, with at least one pair required (CreateBuffer rejects zero buffers). IDL grammar has no 'repeated group' construct, so this file only declares the guaranteed index-0 template (buffer_pda_0/mint_0).", "Each buffer_pda_i must be the canonical PDA for seeds [SETTLEMENT_SEED, mint_i, \"buffer\"]." ], @@ -104,7 +105,9 @@ }, { "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "docs": [ + "The token program that will own the created buffer PDAs. Must be one of the supported token accounts." + ] }, { "name": "buffer_pda_0", @@ -330,6 +333,7 @@ "name": "reclaim_buffer", "docs": [ "Closes one or more buffer PDAs and sends each closed buffer's rent lamports to a reclaim_recipient of the caller's choosing. Only the current holder of the ReclaimAuthority role recorded in the state PDA may authorize this. A buffer that still holds tokens is skipped, not closed, and the instruction still succeeds.", + "Every buffer closed by one instruction must be owned by the single token_program the instruction is handed, so buffers spread across both supported token programs need one instruction each.", "IDL LIMITATION: the real instruction accepts an unbounded number of (buffer_pda, mint) pairs as remaining accounts, one pair per buffer, with at least one pair required (ReclaimBuffer rejects zero buffers). IDL grammar has no 'repeated group' construct, so this file only declares the guaranteed index-0 template (buffer_pda_0/mint_0).", "Each buffer_pda_i must be the canonical PDA for seeds [SETTLEMENT_SEED, mint_i, \"buffer\"]; mint_i is passed only so that derivation can be checked on-chain." ], @@ -387,7 +391,9 @@ }, { "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "docs": [ + "The token program that owns the created buffer PDAs. Must be one of the supported token accounts." + ] }, { "name": "buffer_pda_0", @@ -1041,6 +1047,11 @@ "code": 39, "name": "OrderCreatedOnChainMismatch", "msg": "A created order's intent isn't set with the created_on_chain flag corresponding to the behavior of the invoked order creation instruction." + }, + { + "code": 40, + "name": "BufferSizeUnavailable", + "msg": "CreateBuffer asked the token program how long a token account for a mint has to be and couldn't read the answer, so it can't size the buffer." } ] } diff --git a/programs/settlement/src/create_buffer.rs b/programs/settlement/src/create_buffer.rs index 2dab7e0..fd9318f 100644 --- a/programs/settlement/src/create_buffer.rs +++ b/programs/settlement/src/create_buffer.rs @@ -2,15 +2,18 @@ use cow_settlement_interface::{ instruction::{ - create_buffer::{BufferAccounts, CreateBufferInput, SPL_TOKEN_PROGRAM_ID}, + create_buffer::{BufferAccounts, CreateBufferInput}, InstructionInputParsing, }, pda::{buffer::buffer_pda_seeds, state::state_pda_seeds}, }; -use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; -use pinocchio_token::{instructions::InitializeAccount3, state::Account as TokenAccount}; +use pinocchio::{AccountView, Address, ProgramResult}; +use pinocchio_token::instructions::InitializeAccount3; -use crate::processor::CanonicalPda; +use crate::{ + processor::CanonicalPda, + token::{token_account_len, validate_token_program}, +}; pub fn process_create_buffer( program_id: &Address, @@ -19,12 +22,11 @@ pub fn process_create_buffer( ) -> ProgramResult { let input = CreateBufferInput::parse(instruction_data, accounts)?; - // Only the legacy SPL Token program is supported. The InitializeAccount3 - // CPI targets that program unconditionally; reject a mismatching account - // up front so the caller gets a clear error. - if input.token_program.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } + // Every buffer this instruction creates belongs to the one token program + // it was handed, so reject an unsupported one up front rather than at the + // first CPI. + let token_program = validate_token_program(input.token_program)?; + let token_program_id = token_program.address(); // The buffers' token authority is the settlement state PDA, the single // authority over every buffer. Derive it once for all buffers. @@ -34,8 +36,8 @@ pub fn process_create_buffer( // One buffer per token. `CanonicalPda::create_idempotent` derives the // canonical bump and, by signing the allocation with the buffer seeds, // rejects any `buffer_pda` that isn't the canonical address. The buffer - // is a token account, so it's assigned to the SPL Token program rather - // than to the settlement program. + // is a token account, so it's assigned to the token program rather than + // to the settlement program. // // We don't validate `mint` here. `InitializeAccount3` requires a real, // token-program-owned mint (and special-cases the native mint), so a @@ -45,8 +47,8 @@ pub fn process_create_buffer( program_id, payer: input.payer, pda: buffer_pda, - size: TokenAccount::LEN as u64, - owner: &SPL_TOKEN_PROGRAM_ID, + size: token_account_len(token_program, mint)?, + owner: &token_program_id, seeds: buffer_pda_seeds(mint_key), } .create_idempotent()?; @@ -54,7 +56,8 @@ pub fn process_create_buffer( // An existing buffer is already an initialized token account, so only // initialize a freshly created one. if created { - InitializeAccount3::new(buffer_pda, mint, &state_pda).invoke()?; + InitializeAccount3::new(buffer_pda, mint, &state_pda) + .invoke_with_unverified_program(&token_program_id)?; } } @@ -69,6 +72,7 @@ mod tests { create_buffer_data, NUM_SHARED_ACCOUNTS, }; use cow_settlement_interface::instruction::fixtures::fake_sequential_accounts; + use pinocchio::error::ProgramError; #[test] fn process_create_buffer_propagates_error() { diff --git a/programs/settlement/src/lib.rs b/programs/settlement/src/lib.rs index 1fc2f41..f4224db 100644 --- a/programs/settlement/src/lib.rs +++ b/programs/settlement/src/lib.rs @@ -12,6 +12,7 @@ mod reclaim_buffer; mod reclaim_order; mod remove_solver; mod settle; +mod token; mod transfer_authority; use add_solver::process_add_solver; diff --git a/programs/settlement/src/reclaim_buffer.rs b/programs/settlement/src/reclaim_buffer.rs index 6a1b045..151e6d5 100644 --- a/programs/settlement/src/reclaim_buffer.rs +++ b/programs/settlement/src/reclaim_buffer.rs @@ -7,17 +7,17 @@ use cow_settlement_interface::{ data::state::StateAccount, - instruction::{ - create_buffer::SPL_TOKEN_PROGRAM_ID, reclaim_buffer::ReclaimBufferInput, - InstructionInputParsing, - }, + instruction::{reclaim_buffer::ReclaimBufferInput, InstructionInputParsing}, pda::buffer::find_buffer_pda, Pubkey, Role, SettlementError, }; -use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; -use pinocchio_token::{instructions::CloseAccount, state::Account as TokenAccount}; +use pinocchio::{AccountView, Address, ProgramResult}; +use pinocchio_token::instructions::CloseAccount; -use crate::processor::with_state_pda_signer; +use crate::{ + processor::with_state_pda_signer, + token::{read_token_account, validate_token_program}, +}; pub fn process_reclaim_buffer( program_id: &Address, @@ -32,9 +32,8 @@ pub fn process_reclaim_buffer( buffers, } = ReclaimBufferInput::parse(instruction_data, accounts)?; - if token_program.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } + let token_program = validate_token_program(token_program)?; + let token_program_id = token_program.address(); with_state_pda_signer(program_id, state_pda, |state_signer| { let reclaim_authority_pubkey: Pubkey = @@ -52,9 +51,7 @@ pub fn process_reclaim_buffer( return Err(SettlementError::ReclaimBufferNotCanonical.into()); } - let amount = TokenAccount::from_account_view(buffer_pda) - .map_err(|_| ProgramError::InvalidAccountData)? - .amount(); + let amount = read_token_account(token_program, buffer_pda)?.amount; // A token account can't be closed while it still holds a balance, and this // instruction has no mandate to move those tokens elsewhere or destroy them. @@ -64,7 +61,10 @@ pub fn process_reclaim_buffer( } CloseAccount::new(buffer_pda, reclaim_recipient, state_pda) - .invoke_signed(core::slice::from_ref(state_signer))?; + .invoke_signed_with_unverified_program( + core::slice::from_ref(state_signer), + &token_program_id, + )?; } Ok(()) @@ -83,13 +83,16 @@ mod tests { reclaim_buffer_data, NUM_SHARED_ACCOUNTS, }; use cow_settlement_interface::pda::state::state_pda_seeds; + use cow_settlement_interface::token_program::TokenProgram; use litesvm_token::spl_token::state::{Account as SplTokenAccount, AccountState}; + use pinocchio::error::ProgramError; use solana_program_pack::Pack; use super::*; const AUTHORITY: Address = Address::new_from_array([101; 32]); const MANAGER: Address = Address::new_from_array([102; 32]); const UNRELATED: Address = Address::new_from_array([254; 32]); + const SPL_TOKEN_PROGRAM_ID: Address = TokenProgram::SplToken.address(); /// Number of accounts in a one-buffer reclaim: the shared ones plus a /// single `(buffer_pda, mint)` pair. diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index 4100403..19d6927 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -29,11 +29,12 @@ use pinocchio::{ }; use pinocchio_token::{instructions::Transfer, state::Account as TokenAccount}; -use crate::processor::{ - check_state_pda, is_cpi_call, require_solver, with_state_pda_signer_from_bump, +use crate::{ + processor::{check_state_pda, is_cpi_call, require_solver, with_state_pda_signer_from_bump}, + token::validate_token_program, }; -use super::{validate_counterpart, validate_token_program_account}; +use super::validate_counterpart; pub fn process_begin_settle( program_id: &Address, @@ -75,7 +76,7 @@ pub fn process_begin_settle( let finalize_ix = instructions.load_instruction_at(usize::from(input.finalize_ix_index))?; - validate_token_program_account(input.token_program_account)?; + validate_token_program(input.token_program_account)?; with_state_pda_signer_from_bump(state_bump, |signer| { settle_orders( diff --git a/programs/settlement/src/settle/finalize.rs b/programs/settlement/src/settle/finalize.rs index df190ee..ea20b01 100644 --- a/programs/settlement/src/settle/finalize.rs +++ b/programs/settlement/src/settle/finalize.rs @@ -12,9 +12,12 @@ use pinocchio::{ }; use pinocchio_token::instructions::Transfer; -use crate::processor::{is_cpi_call, with_state_pda_signer}; +use crate::{ + processor::{is_cpi_call, with_state_pda_signer}, + token::validate_token_program, +}; -use super::{validate_counterpart, validate_token_program_account}; +use super::validate_counterpart; pub fn process_finalize_settle( program_id: &Address, @@ -44,7 +47,7 @@ pub fn process_finalize_settle( // the canonical buffer for the order's buy mint. Nothing is left to check // here, so `push_funds` only executes the transfers. - validate_token_program_account(input.token_program_account)?; + validate_token_program(input.token_program_account)?; with_state_pda_signer(program_id, input.state_pda_account, |state_pda_signer| { push_funds(input.state_pda_account, state_pda_signer, input.pushes) diff --git a/programs/settlement/src/settle/mod.rs b/programs/settlement/src/settle/mod.rs index 25a01d7..f76737a 100644 --- a/programs/settlement/src/settle/mod.rs +++ b/programs/settlement/src/settle/mod.rs @@ -3,12 +3,10 @@ use std::ops::Deref; use cow_settlement_interface::{ - instruction::{create_buffer::SPL_TOKEN_PROGRAM_ID, settle::recover_counterpart}, - recover_discriminator, SettlementError, SettlementInstruction, -}; -use pinocchio::{ - error::ProgramError, sysvars::instructions::Instructions, AccountView, Address, ProgramResult, + instruction::settle::recover_counterpart, recover_discriminator, SettlementError, + SettlementInstruction, }; +use pinocchio::{sysvars::instructions::Instructions, Address, ProgramResult}; mod begin; mod finalize; @@ -44,13 +42,3 @@ fn validate_counterpart>( } Ok(()) } - -/// Validate that `token_program_account` is the legacy SPL Token program, which -/// every settlement transfer is issued against. -#[must_use = "ignoring the output may lead to an unintended on-chain state"] -fn validate_token_program_account(token_program_account: &AccountView) -> ProgramResult { - if token_program_account.address() != &SPL_TOKEN_PROGRAM_ID { - return Err(ProgramError::IncorrectProgramId); - } - Ok(()) -} diff --git a/programs/settlement/src/token.rs b/programs/settlement/src/token.rs new file mode 100644 index 0000000..5f284c7 --- /dev/null +++ b/programs/settlement/src/token.rs @@ -0,0 +1,243 @@ +//! Token-program validation and token-account reads + +use cow_settlement_interface::{ + token_program::TokenProgram::{self, SplToken}, + SettlementError, +}; +use pinocchio::{cpi::get_return_data, error::ProgramError, AccountView}; +use pinocchio_token::instructions::GetAccountDataSize; + +/// The length of a SPL token program account. Token2022 extensions may make +/// the actual token account longer than this. +const BASE_TOKEN_ACCOUNT_LEN: u64 = pinocchio_token::state::Account::LEN as u64; + +/// Validate that `token_program_account` is a token program this program may +/// issue CPIs against, returning the program for the instruction to target. +#[must_use = "not consuming skips validation"] +pub fn validate_token_program( + token_program_account: &AccountView, +) -> Result { + TokenProgram::try_from(token_program_account.address()) +} + +/// The data length a token account holding `mint` has to be allocated at. +pub fn token_account_len( + token_program: TokenProgram, + mint: &AccountView, +) -> Result { + // Early return for SPL token (saves the GetAccountDataSize CPI call) + if token_program == SplToken { + return Ok(BASE_TOKEN_ACCOUNT_LEN); + } + + let token_program = token_program.address(); + // SPL token provides a function to get the actual required account data size + GetAccountDataSize::new(mint).invoke_with_unverified_program(&token_program)?; + + let reported = get_return_data().ok_or(SettlementError::BufferSizeUnavailable)?; + if reported.program_id() != &token_program { + return Err(SettlementError::BufferSizeUnavailable.into()); + } + let length: [u8; 8] = reported + .as_slice() + .try_into() + .map_err(|_| SettlementError::BufferSizeUnavailable)?; + let length = u64::from_le_bytes(length); + + Ok(length) +} + +/// The base-layout fields of a token account, as read by +/// [`read_token_account`]. +/// For our purposes, we only need the `amount`. +pub struct TokenAccount { + pub amount: u64, +} + +/// Read the base fields of the token account at `account`, which must be owned +/// by `token_program`. +pub fn read_token_account( + token_program: TokenProgram, + account: &AccountView, +) -> Result { + let amount = match token_program { + TokenProgram::SplToken => { + pinocchio_token::state::Account::from_account_view(account)?.amount() + } + TokenProgram::Token2022 => { + pinocchio_token_2022::state::Account::from_account_view(account)?.amount() + } + }; + Ok(TokenAccount { amount }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cow_settlement_interface::{ + fixtures::pubkey_from_seed, + instruction::fixtures::{fake_account, fake_account_owned_by}, + }; + use litesvm_token::spl_token::state::{Account as SplTokenAccount, AccountState}; + use pinocchio::Address; + use pinocchio_token::state::Mint; + use pinocchio_token_2022::state::AccountType; + use solana_program_pack::Pack; + + /// The length of a token account holding nothing but the base layout. Both + /// programs share it: it is Token-2022's `BASE_LEN` and the whole of a + /// legacy account. + const BASE_LEN: usize = pinocchio_token::state::Account::LEN; + + /// The base layout of a token account holding `amount` of `mint` for + /// `owner`, encoded by the SPL token program's own packer so the fixture + /// cannot drift from the layout the readers parse. + fn base_account_layout(mint: Address, owner: Address, amount: u64) -> Vec { + let mut data = vec![0u8; BASE_LEN]; + SplTokenAccount { + mint, + owner, + amount, + state: AccountState::Initialized, + ..Default::default() + } + .pack_into_slice(&mut data); + data + } + + /// The addresses the off-chain crate offers are the ones the on-chain token + /// crates CPI into. Both sides name the same programs from their own + /// dependency, so this is what keeps them from drifting apart. + #[test] + fn interface_and_pinocchio_agree_on_the_program_ids() { + for program in TokenProgram::ALL { + let pinocchio_id = match program { + TokenProgram::SplToken => pinocchio_token::ID, + TokenProgram::Token2022 => pinocchio_token_2022::ID, + }; + assert_eq!(program.address(), pinocchio_id); + } + } + + /// The base layout is the same under both programs, so one reader's idea of + /// its length is the other's too. + #[test] + fn both_programs_share_the_base_layout_length() { + assert_eq!(BASE_LEN, pinocchio_token_2022::state::Account::BASE_LEN); + } + + #[test] + fn token_account_len_is_base_length_for_spl_program() { + let mint = fake_account_owned_by( + pubkey_from_seed("mint"), + SplToken.address(), + &[0u8; Mint::LEN], + ); + assert_eq!( + token_account_len(SplToken, &mint), + Ok(BASE_TOKEN_ACCOUNT_LEN), + "SPL owned mint should yield ase length", + ); + } + + #[test] + fn token_account_len_reports_unavailable_without_an_answer() { + let mint = fake_account_owned_by( + pubkey_from_seed("mint"), + TokenProgram::Token2022.address(), + &[0u8; Mint::LEN + 1], + ); + assert_eq!( + token_account_len(TokenProgram::Token2022, &mint).err(), + Some(SettlementError::BufferSizeUnavailable.into()), + ); + } + + #[test] + fn validate_token_program_accepts_every_supported_program() { + for program in TokenProgram::ALL { + let account = fake_account(program.address()); + assert_eq!(validate_token_program(&account), Ok(program)); + } + } + + #[test] + fn validate_token_program_rejects_unrelated_program() { + let account = fake_account(pubkey_from_seed("not a token program")); + assert_eq!( + validate_token_program(&account), + Err(ProgramError::IncorrectProgramId), + ); + } + + #[test] + fn read_token_account_reads_a_base_layout_account() { + let mint = pubkey_from_seed("mint"); + let owner = pubkey_from_seed("owner"); + for program in TokenProgram::ALL { + let account = fake_account_owned_by( + pubkey_from_seed("token account"), + program.address(), + &base_account_layout(mint, owner, 4_200), + ); + let read = read_token_account(program, &account) + .unwrap_or_else(|error| panic!("{program:?} account should read: {error:?}")); + assert_eq!(read.amount, 4_200); + } + } + + #[test] + fn read_token_account_reads_past_token_2022_extensions() { + let mint = pubkey_from_seed("extended mint"); + let owner = pubkey_from_seed("extended owner"); + let mut data = base_account_layout(mint, owner, 7); + // Extensions are preceded by the account-type marker, which is what + // distinguishes a longer account from a mint of the same size. + data.push(AccountType::Account as u8); + data.extend_from_slice(&[0xab; 16]); + + let account = fake_account_owned_by( + pubkey_from_seed("token account"), + TokenProgram::Token2022.address(), + &data, + ); + let read = read_token_account(TokenProgram::Token2022, &account) + .expect("an extended Token-2022 account should read"); + assert_eq!(read.amount, 7); + } + + #[test] + fn read_token_account_rejects_an_extended_mint() { + let mut data = base_account_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 7); + data.push(AccountType::Mint as u8); + + let account = fake_account_owned_by( + pubkey_from_seed("mint account"), + TokenProgram::Token2022.address(), + &data, + ); + assert_eq!( + read_token_account(TokenProgram::Token2022, &account).err(), + Some(ProgramError::InvalidAccountData), + ); + } + + #[test] + fn read_token_account_rejects_the_other_programs_account() { + for [program, other] in [ + [TokenProgram::SplToken, TokenProgram::Token2022], + [TokenProgram::Token2022, TokenProgram::SplToken], + ] { + let account = fake_account_owned_by( + pubkey_from_seed("token account"), + other.address(), + &base_account_layout(pubkey_from_seed("mint"), pubkey_from_seed("owner"), 0), + ); + assert_eq!( + read_token_account(program, &account).err(), + Some(ProgramError::InvalidAccountData), + "an account owned by {other:?} should not read under {program:?}", + ); + } + } +} diff --git a/programs/settlement/tests/common/buffer.rs b/programs/settlement/tests/common/buffer.rs index 2dddd41..b8f9070 100644 --- a/programs/settlement/tests/common/buffer.rs +++ b/programs/settlement/tests/common/buffer.rs @@ -3,6 +3,7 @@ use cow_settlement_client::cow_settlement_interface::pda::buffer::find_buffer_pda; use cow_settlement_client::cow_settlement_interface::Instruction; use cow_settlement_client::instructions::CreateBuffers; +use cow_settlement_interface::token_program::TokenProgram; use litesvm::LiteSVM; use solana_sdk::{ pubkey::Pubkey, @@ -25,6 +26,18 @@ pub fn ensure_buffer_exists( program_id: &Pubkey, payer: &Keypair, mint: &Pubkey, +) -> Pubkey { + ensure_buffer_exists_for(svm, program_id, payer, mint, TokenProgram::SplToken) +} + +/// [`ensure_buffer_exists`] under a token program of the caller's choosing, for +/// the tests that need a buffer belonging to Token-2022. +pub fn ensure_buffer_exists_for( + svm: &mut LiteSVM, + program_id: &Pubkey, + payer: &Keypair, + mint: &Pubkey, + token_program: TokenProgram, ) -> Pubkey { let pda = buffer_pda(program_id, mint); if svm.get_account(&pda).is_some() { @@ -33,6 +46,7 @@ pub fn ensure_buffer_exists( let ix = Instruction::from(CreateBuffers { program_id: *program_id, payer: payer.pubkey(), + token_program, mints: &[*mint], }); let tx = Transaction::new_signed_with_payer( diff --git a/programs/settlement/tests/common/mod.rs b/programs/settlement/tests/common/mod.rs index cb54dcc..995cc7b 100644 --- a/programs/settlement/tests/common/mod.rs +++ b/programs/settlement/tests/common/mod.rs @@ -13,9 +13,11 @@ pub mod pda; pub mod settlement; pub mod state; pub mod token; +pub mod token_2022; use cow_settlement_client::instructions::{AddSolver, Initialize}; use cow_settlement_interface::pda::state::find_state_pda; +use cow_settlement_interface::token_program::TokenProgram; use cow_settlement_interface::Instruction; use cow_settlement_interface::SettlementError; use litesvm::{types::TransactionMetadata, LiteSVM}; @@ -34,6 +36,10 @@ pub const PROGRAM_SO: &str = concat!( "/../../target/deploy/cow_settlement.so" ); +/// The legacy SPL Token program, which the tests create their buffers and +/// token accounts under unless they exercise Token-2022 specifically. +pub const SPL_TOKEN_PROGRAM_ID: Pubkey = TokenProgram::SplToken.address(); + pub const CPI_CALLER_SO: &str = concat!( env!("CARGO_MANIFEST_DIR"), "/../../target/deploy/test_cpi_caller.so" diff --git a/programs/settlement/tests/common/token.rs b/programs/settlement/tests/common/token.rs index a6e7b89..709b229 100644 --- a/programs/settlement/tests/common/token.rs +++ b/programs/settlement/tests/common/token.rs @@ -3,7 +3,7 @@ use cow_settlement_client::cow_settlement_interface::pda::state::find_state_pda; use litesvm::{types::TransactionMetadata, LiteSVM}; use litesvm_token::{ - spl_token::{instruction::initialize_mint2, state::Mint}, + spl_token::{instruction::initialize_mint2, native_mint, state::Mint}, Approve, CreateAccount, CreateAssociatedTokenAccount, MintTo, Transfer, TOKEN_ID, }; use solana_program_pack::Pack; @@ -24,10 +24,16 @@ use super::unique_keypair; /// buffer PDA, so a random one makes buffer bumps — and the compute cost of /// deriving them — vary between runs. See [`super::unique_pubkey`]. pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { + create_mint_at(svm, payer, &unique_keypair()) +} + +/// [`create_mint`] at `mint`'s address rather than a fresh one. Lets a test +/// reclaim an address a Token-2022 mint was just closed at, which is the only +/// way a legacy mint can end up where a Token-2022 one used to be. +pub fn create_mint_at(svm: &mut LiteSVM, payer: &Keypair, mint: &Keypair) -> Pubkey { /// `litesvm_token::CreateMint`'s default, kept so the two agree. const DECIMALS: u8 = 8; - let mint = unique_keypair(); let create = system_create_account( &payer.pubkey(), &mint.pubkey(), @@ -40,7 +46,7 @@ pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { let tx = Transaction::new_signed_with_payer( &[create, initialize], Some(&payer.pubkey()), - &[payer, &mint], + &[payer, mint], svm.latest_blockhash(), ); svm.send_transaction(tx) @@ -48,6 +54,24 @@ pub fn create_mint(svm: &mut LiteSVM, payer: &Keypair) -> Pubkey { mint.pubkey() } +/// Seed the wrapped-SOL mint account, which `LiteSVM` does not create. +pub fn create_native_mint(svm: &mut LiteSVM) { + /// The native mint's fixed decimals, matching `spl_token::native_mint`. + const DECIMALS: u8 = 9; + + let mut data = vec![0u8; Mint::LEN]; + Mint { + mint_authority: None.into(), + supply: 0, + decimals: DECIMALS, + is_initialized: true, + freeze_authority: None.into(), + } + .pack_into_slice(&mut data); + let token_program = Pubkey::new_from_array(TOKEN_ID.to_bytes()); + super::create_account_at(svm, native_mint::ID, &token_program, &data); +} + /// Create an initialized SPL token account for `mint` whose SPL owner is /// `owner`, funded by `payer`, and return its address. Each call produces a /// fresh account, so the same `owner` can hold several accounts for one `mint`. diff --git a/programs/settlement/tests/common/token_2022.rs b/programs/settlement/tests/common/token_2022.rs new file mode 100644 index 0000000..5706523 --- /dev/null +++ b/programs/settlement/tests/common/token_2022.rs @@ -0,0 +1,185 @@ +//! Token-2022 helpers for the settlement integration tests. +//! +//! Unlike the legacy program, Token-2022 lets a mint be closed and its address +//! reused for something else entirely. A buffer PDA is derived from the mint +//! address alone, so a buffer outlives the mint it was created for. These +//! helpers drive that lifecycle: create a mint under a chosen extension set, +//! close it, and put a different mint at the same address. + +use cow_settlement_interface::token_program::TokenProgram; +use litesvm::LiteSVM; +use solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use solana_system_interface::instruction::create_account as system_create_account; +use spl_token_2022_interface::{ + extension::{transfer_fee::instruction::initialize_transfer_fee_config, ExtensionType}, + instruction::{ + close_account, initialize_mint2, initialize_mint_close_authority, + initialize_non_transferable_mint, + }, + state::{Account, Mint}, +}; + +/// The Token-2022 program, the counterpart of [`super::SPL_TOKEN_PROGRAM_ID`]. +const TOKEN_2022_PROGRAM_ID: Pubkey = TokenProgram::Token2022.address(); + +/// Decimals every test mint carries, matching [`super::token::create_mint`] so +/// a legacy and a Token-2022 mint differ only in their program. +const DECIMALS: u8 = 8; + +/// Transfer-fee parameters for [`Extensions::WithTransferFee`]. Arbitrary; +/// nothing reads them back, but `InitializeTransferFeeConfig` demands values. +const FEE_BASIS_POINTS: u16 = 50; +const MAXIMUM_FEE: u64 = 1_000; + +/// The extension set a test mint is created with. +#[derive(Clone, Copy, Debug)] +pub enum Extensions { + CloseAuthorityOnly, + CloseAuthorityAndNonTransferable, + CloseAuthorityAndTransferFee, +} + +impl Extensions { + /// The extensions the mint itself is initialized with. + fn mint(self) -> &'static [ExtensionType] { + match self { + Self::CloseAuthorityOnly => &[ExtensionType::MintCloseAuthority], + Self::WithNonTransferable => &[ + ExtensionType::MintCloseAuthority, + ExtensionType::NonTransferable, + ], + Self::WithTransferFee => &[ + ExtensionType::MintCloseAuthority, + ExtensionType::TransferFeeConfig, + ], + } + } + + /// The extensions Token-2022 requires of a token account holding the mint. + /// Spelled out rather than derived from [`Self::mint`], so the length a test + /// expects is stated independently of the program's own bookkeeping. + fn token_account(self) -> &'static [ExtensionType] { + match self { + Self::CloseAuthorityOnly => &[], + Self::WithNonTransferable => &[ + ExtensionType::NonTransferableAccount, + ExtensionType::ImmutableOwner, + ], + Self::WithTransferFee => &[ExtensionType::TransferFeeAmount], + } + } + + /// The data length a token account holding the mint has to be allocated at, + /// which is what `create_buffer` asks the token program for. + pub fn token_account_len(self) -> usize { + ExtensionType::try_calculate_account_len::(self.token_account()) + .expect("every account extension used here has a fixed length") + } + + /// The instructions initializing the extensions on `mint`, with `authority` + /// filling every authority they ask for. Token-2022 requires all of them to + /// run before `InitializeMint`, and insists the mint be allocated at exactly + /// the length they need. + fn initializers(self, mint: &Pubkey, authority: &Pubkey) -> Vec { + self.mint() + .iter() + .map(|extension| { + match extension { + ExtensionType::MintCloseAuthority => initialize_mint_close_authority( + &TOKEN_2022_PROGRAM_ID, + mint, + Some(authority), + ), + ExtensionType::NonTransferable => { + initialize_non_transferable_mint(&TOKEN_2022_PROGRAM_ID, mint) + } + ExtensionType::TransferFeeConfig => initialize_transfer_fee_config( + &TOKEN_2022_PROGRAM_ID, + mint, + Some(authority), + Some(authority), + FEE_BASIS_POINTS, + MAXIMUM_FEE, + ), + other => panic!("no initializer is wired up for {other:?}"), + } + .expect("extension initializer should build") + }) + .collect() + } +} + +/// Create a Token-2022 mint at `mint`'s address carrying `extensions`, with +/// `payer` as both its mint authority and its close authority, and return the +/// address. Taking the keypair rather than generating one lets a test close the +/// mint and put something else back at the same address. +pub fn create_mint( + svm: &mut LiteSVM, + payer: &Keypair, + mint: &Keypair, + extensions: Extensions, +) -> Pubkey { + let space = ExtensionType::try_calculate_account_len::(extensions.mint()) + .expect("every mint extension used here has a fixed length"); + let mut instructions = vec![system_create_account( + &payer.pubkey(), + &mint.pubkey(), + svm.minimum_balance_for_rent_exemption(space), + space as u64, + &TOKEN_2022_PROGRAM_ID, + )]; + instructions.extend(extensions.initializers(&mint.pubkey(), &payer.pubkey())); + instructions.push( + initialize_mint2( + &TOKEN_2022_PROGRAM_ID, + &mint.pubkey(), + &payer.pubkey(), + None, + DECIMALS, + ) + .expect("initialize_mint2 should build"), + ); + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, mint], + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .expect("Token-2022 mint creation should succeed"); + mint.pubkey() +} + +/// Close `mint`, whose close authority must be `payer`, refunding its rent to +/// `payer`. Token-2022 hands the emptied account back to the System program, so +/// the address is free for [`create_mint`] or [`super::token::create_mint_at`] +/// to claim again. +pub fn close_mint(svm: &mut LiteSVM, payer: &Keypair, mint: &Pubkey) { + let ix = close_account( + &TOKEN_2022_PROGRAM_ID, + mint, + &payer.pubkey(), + &payer.pubkey(), + &[], + ) + .expect("close_account should build"); + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[payer], + svm.latest_blockhash(), + ); + svm.send_transaction(tx) + .expect("closing the mint should succeed"); + assert!( + svm.get_account(mint) + .is_none_or(|account| account.data.is_empty()), + "a closed mint must leave no data behind at its address", + ); +} diff --git a/programs/settlement/tests/create_buffer.rs b/programs/settlement/tests/create_buffer.rs index f0aee17..0c7546e 100644 --- a/programs/settlement/tests/create_buffer.rs +++ b/programs/settlement/tests/create_buffer.rs @@ -1,11 +1,12 @@ use cow_settlement_client::cow_settlement_interface::{ - instruction::create_buffer::{CreateBuffers as CreateBuffersRaw, SPL_TOKEN_PROGRAM_ID}, + instruction::create_buffer::CreateBuffers as CreateBuffersRaw, pda::{ buffer::{buffer_pda_seeds, find_buffer_pda}, state::find_state_pda, }, }; use cow_settlement_client::instructions::CreateBuffers; +use cow_settlement_interface::token_program::TokenProgram; use litesvm::LiteSVM; use litesvm_token::{ get_spl_account, @@ -28,6 +29,7 @@ use solana_sdk::{ use crate::common::{ benchmark::{send_transaction_metered, BenchLabel}, + token_2022::Extensions, unique_keypair, unique_pubkey, }; @@ -43,6 +45,7 @@ fn happy_path_creates_initialized_buffer_token_account() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -53,7 +56,8 @@ fn happy_path_creates_initialized_buffer_token_account() { .get_account(&buffer_pda) .expect("buffer PDA should exist after create_buffer"); assert_eq!( - account.owner, SPL_TOKEN_PROGRAM_ID, + account.owner, + TokenProgram::SplToken.address(), "buffer must be owned by the SPL Token program" ); assert_eq!( @@ -110,6 +114,7 @@ fn buffer_can_receive_tokens() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -138,16 +143,16 @@ fn buffer_can_receive_tokens() { #[test] fn happy_path_creates_native_token_buffer() { - // The native mint is special-cased by the token program: it's recognized by - // key (no mint-account validation) and the buffer is initialized as a - // wrapped-SOL account. Since we fund exactly the rent-exempt minimum, the - // wrapped balance starts at zero. + // `InitializeAccount` special-cases the native mint: it's recognized by key + // and the buffer is initialized as a wrapped-SOL account. Since we fund + // exactly the rent-exempt minimum, the wrapped balance starts at zero. let (mut svm, program_id, payer) = common::setup(); let (buffer_pda, _bump) = find_buffer_pda(&program_id, &native_mint::ID); let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[native_mint::ID], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -183,6 +188,7 @@ fn happy_path_creates_multiple_buffers_in_one_instruction() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &mints, }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -195,7 +201,8 @@ fn happy_path_creates_multiple_buffers_in_one_instruction() { .get_account(&buffer_pda) .expect("each buffer PDA should exist after create_buffers"); assert_eq!( - account.owner, SPL_TOKEN_PROGRAM_ID, + account.owner, + TokenProgram::SplToken.address(), "each buffer must be owned by the SPL Token program" ); assert_eq!( @@ -227,6 +234,7 @@ fn rejects_no_buffers() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -255,6 +263,7 @@ fn rejects_arbitrary_wrong_buffer_pda() { let ix = CreateBuffersRaw { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken.address(), buffers: &[(wrong_pda, mint)], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -275,6 +284,7 @@ fn rejects_non_canonical_bump_pda() { let ix = CreateBuffersRaw { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken.address(), buffers: &[(non_canonical_pda, mint)], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -291,12 +301,14 @@ fn rejects_non_spl_token_program() { let mut ix: Instruction = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint], } .into(); let token_program_index = 2; assert_eq!( - ix.accounts[token_program_index].pubkey, SPL_TOKEN_PROGRAM_ID, + ix.accounts[token_program_index].pubkey, + TokenProgram::SplToken.address(), "sanity: should replace token program" ); ix.accounts[token_program_index].pubkey = unique_pubkey(); @@ -334,6 +346,7 @@ fn rejects_invalid_mint() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[not_a_mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -367,6 +380,7 @@ fn creates_buffer_when_address_is_prefunded() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint], }; common::signed_tx(svm, &payer, &payer, ix) @@ -383,6 +397,7 @@ fn recreating_same_buffer_is_idempotent() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint], }; common::signed_tx(svm, &payer, &payer, ix) @@ -400,6 +415,7 @@ fn batch_with_existing_buffer_passes_with_no_changes() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[existing], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -414,6 +430,7 @@ fn batch_with_existing_buffer_passes_with_no_changes() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[fresh, existing], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -449,6 +466,7 @@ fn one_failing_buffer_reverts_the_whole_batch() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[fresh, not_a_mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -483,6 +501,7 @@ fn same_mint_twice_in_one_instruction_is_idempotent() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint, mint], }; let tx = common::signed_tx(&svm, &payer, &payer, ix); @@ -495,6 +514,49 @@ fn same_mint_twice_in_one_instruction_is_idempotent() { ); } +#[test] +fn sizes_a_token_2022_buffer_to_the_extensions_its_mint_forces() { + let (mut svm, program_id, payer) = common::setup(); + + for extensions in [ + Extensions::CloseAuthorityOnly, + Extensions::WithNonTransferable, + Extensions::WithTransferFee, + ] { + let mint = common::token_2022::create_mint(&mut svm, &payer, &unique_keypair(), extensions); + let (buffer_pda, _bump) = find_buffer_pda(&program_id, &mint); + + let ix = CreateBuffers { + program_id, + payer: payer.pubkey(), + token_program: TokenProgram::Token2022, + mints: &[mint], + }; + let tx = common::signed_tx(&svm, &payer, &payer, ix); + svm.send_transaction(tx).unwrap_or_else(|err| { + panic!( + "create_buffer should succeed for {extensions:?}: {:?}", + err.err + ) + }); + + let account = svm + .get_account(&buffer_pda) + .expect("buffer PDA should exist after create_buffer"); + assert_eq!( + account.owner, + TokenProgram::Token2022.address(), + "a {extensions:?} buffer must be owned by the Token-2022 program" + ); + assert_eq!( + account.data.len(), + extensions.token_account_len(), + "a {extensions:?} buffer must be allocated at the length its extensions imply" + ); + common::assert_rent_exempt(&svm, &account); + } +} + /// Largest number of buffers a single ALT-backed `create_buffers` transaction /// can carry, bounded by the transaction account-lock limit (litesvm and current /// mainnet both cap this at 64). @@ -508,6 +570,7 @@ fn max_buffers_via_lookup_table(svm: &mut LiteSVM, program_id: &Pubkey, payer: & let ix = CreateBuffersRaw { program_id: *program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken.address(), buffers: &buffers, }; common::lookup_table::lookup_table_tx(svm, payer, ix) @@ -575,6 +638,7 @@ fn max_buffers_in_one_instruction() { let ix = CreateBuffers { program_id, payer: payer.pubkey(), + token_program: TokenProgram::SplToken, mints: &mints, }; let tx = common::lookup_table::lookup_table_tx(&mut svm, &payer, ix); diff --git a/programs/settlement/tests/reclaim_buffer.rs b/programs/settlement/tests/reclaim_buffer.rs index ad5607d..13c5413 100644 --- a/programs/settlement/tests/reclaim_buffer.rs +++ b/programs/settlement/tests/reclaim_buffer.rs @@ -1,4 +1,5 @@ use cow_settlement_client::instructions::ReclaimBuffer; +use cow_settlement_interface::token_program::TokenProgram; use cow_settlement_interface::Instruction; use cow_settlement_interface::{ instruction::reclaim_buffer::ReclaimBuffer as ReclaimBufferRaw, pda::buffer::find_buffer_pda, @@ -13,6 +14,7 @@ use solana_sdk::{ use crate::common::benchmark::{send_transaction_metered, BenchLabel}; use crate::common::buffer::ensure_buffer_exists; +use crate::common::token_2022::Extensions; use crate::common::{ assert_instruction_error, to_instruction_error, unique_pubkey, InitializedParams, }; @@ -46,6 +48,7 @@ fn happy_path_reclaims_to_a_recipient_chosen_by_the_authority() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -93,6 +96,7 @@ fn happy_path_reclaims_empty_buffer_to_the_authority_itself() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -132,6 +136,7 @@ fn funded_buffer_is_skipped() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -177,6 +182,7 @@ fn reclaims_to_the_settlements_own_state_pda() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -225,6 +231,7 @@ fn reclaims_multiple_buffers_skipping_funded() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint_a, mint_b], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -261,6 +268,7 @@ fn rejects_the_same_buffer_twice_in_one_instruction() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, + token_program: TokenProgram::SplToken, mints: &[mint, mint], }; let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); @@ -290,6 +298,7 @@ fn rejects_when_signer_is_not_the_configured_reclaim_authority() { program_id, reclaim_authority: impostor.pubkey(), reclaim_recipient: impostor.pubkey(), + token_program: TokenProgram::SplToken, mints: &[mint], }; let tx = common::signed_tx(&svm, &payer, &impostor, ix); @@ -321,6 +330,7 @@ fn rejects_when_the_reclaim_authority_does_not_sign() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: recipient, + token_program: TokenProgram::SplToken, mints: &[mint], }); @@ -346,6 +356,141 @@ fn rejects_when_the_reclaim_authority_does_not_sign() { ); } +/// Set up a Token-2022 mint with a close authority and its buffer, then close +/// the mint so `reopen` can claim the address. Returns the mint and its buffer. +fn buffer_whose_mint_was_reopened( + svm: &mut LiteSVM, + program_id: &Pubkey, + payer: &Keypair, + reopen: impl FnOnce(&mut LiteSVM, &Keypair, &Keypair), +) -> (Pubkey, Pubkey) { + let mint_keypair = common::unique_keypair(); + let mint = + common::token_2022::create_mint(svm, payer, &mint_keypair, Extensions::CloseAuthorityOnly); + let buffer_pda = common::buffer::ensure_buffer_exists_for( + svm, + program_id, + payer, + &mint, + TokenProgram::Token2022, + ); + + common::token_2022::close_mint(svm, payer, &mint); + reopen(svm, payer, &mint_keypair); + + (mint, buffer_pda) +} + +#[test] +fn reclaims_a_buffer_whose_mint_was_reopened_with_another_extension() { + let ( + mut svm, + InitializedParams { + program_id, + payer, + reclaim: reclaim_authority, + .. + }, + ) = common::setup_init(); + + let (mint, buffer_pda) = buffer_whose_mint_was_reopened( + &mut svm, + &program_id, + &payer, + |svm, payer, mint_keypair| { + common::token_2022::create_mint( + svm, + payer, + mint_keypair, + Extensions::WithNonTransferable, + ); + }, + ); + + let buffer_lamports_before = svm + .get_account(&buffer_pda) + .expect("buffer must exist before reclaim") + .lamports; + let recipient_before = common::lamports(&svm, &reclaim_authority.pubkey()); + + let ix = ReclaimBuffer { + program_id, + reclaim_authority: reclaim_authority.pubkey(), + reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::Token2022, + mints: &[mint], + }; + let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); + svm.send_transaction(tx) + .expect("reclaim_buffer should succeed for a reopened mint"); + + assert!( + svm.get_account(&buffer_pda).is_none(), + "buffer PDA must be closed after reclaim" + ); + assert_eq!( + common::lamports(&svm, &reclaim_authority.pubkey()) - recipient_before, + buffer_lamports_before, + "the rent of a buffer stranded by a reopened mint must still be recoverable" + ); +} + +#[test] +fn reclaims_a_buffer_whose_mint_was_reopened_as_a_legacy_mint() { + let ( + mut svm, + InitializedParams { + program_id, + payer, + reclaim: reclaim_authority, + .. + }, + ) = common::setup_init(); + + let (mint, buffer_pda) = buffer_whose_mint_was_reopened( + &mut svm, + &program_id, + &payer, + |svm, payer, mint_keypair| { + common::token::create_mint_at(svm, payer, mint_keypair); + }, + ); + assert_eq!( + svm.get_account(&mint) + .expect("the reopened mint should exist") + .owner, + common::SPL_TOKEN_PROGRAM_ID, + "sanity: the mint must now belong to the legacy program" + ); + + let buffer_lamports_before = svm + .get_account(&buffer_pda) + .expect("buffer must exist before reclaim") + .lamports; + let recipient_before = common::lamports(&svm, &reclaim_authority.pubkey()); + + let ix = ReclaimBuffer { + program_id, + reclaim_authority: reclaim_authority.pubkey(), + reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::Token2022, + mints: &[mint], + }; + let tx = common::signed_tx(&svm, &payer, &reclaim_authority, ix); + svm.send_transaction(tx) + .expect("reclaim_buffer should succeed for a mint reopened as legacy"); + + assert!( + svm.get_account(&buffer_pda).is_none(), + "buffer PDA must be closed after reclaim" + ); + assert_eq!( + common::lamports(&svm, &reclaim_authority.pubkey()) - recipient_before, + buffer_lamports_before, + "the rent of a buffer whose mint turned legacy must still be recoverable" + ); +} + fn max_buffers_reclaim_via_lookup_table( svm: &mut LiteSVM, program_id: &Pubkey, @@ -360,6 +505,7 @@ fn max_buffers_reclaim_via_lookup_table( state_pda, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::SplToken.address(), buffers: &buffers, }; common::lookup_table::lookup_table_tx(svm, reclaim_authority, ix) @@ -438,6 +584,7 @@ fn max_buffers_in_one_instruction() { program_id, reclaim_authority: reclaim_authority.pubkey(), reclaim_recipient: reclaim_authority.pubkey(), + token_program: TokenProgram::SplToken, mints: &mints, }; let tx = common::lookup_table::lookup_table_tx(&mut svm, &reclaim_authority, ix); diff --git a/test-cli/src/cmd/settle.rs b/test-cli/src/cmd/settle.rs index 2d7bb5a..9efb0b8 100644 --- a/test-cli/src/cmd/settle.rs +++ b/test-cli/src/cmd/settle.rs @@ -4,6 +4,7 @@ use cow_settlement_client::{ cow_settlement_interface::{ data::{intent::OrderIntent, order::OrderAccount}, pda::buffer::find_buffer_pda, + token_program::TokenProgram, Pubkey, }, instructions::{ @@ -301,6 +302,8 @@ fn prepare_setup_ixs( CreateBuffers { program_id: ctx.program_id, payer: ctx.payer.pubkey(), + // The CLI only resolves tokens on the legacy program for now. + token_program: TokenProgram::SplToken, mints: &mint_buffers_to_create.into_iter().collect::>(), } .into(),