diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 4af5eaa4607..a1383cfb35e 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -3722,4 +3722,25 @@ where )); self.process_ixs(&instructions, signing_keypairs).await } + + /// Update a token-group max size on a mint + pub async fn token_group_update_max_size( + &self, + update_authority: &Pubkey, + new_max_size: u32, + signing_keypairs: &S, + ) -> TokenResult { + self.process_ixs( + &[ + spl_token_group_interface::instruction::update_group_max_size( + &self.program_id, + &self.pubkey, + update_authority, + new_max_size, + ), + ], + signing_keypairs, + ) + .await + } } diff --git a/token/program-2022-test/tests/token_group_update_max_size.rs b/token/program-2022-test/tests/token_group_update_max_size.rs new file mode 100644 index 00000000000..9e8382fae20 --- /dev/null +++ b/token/program-2022-test/tests/token_group_update_max_size.rs @@ -0,0 +1,249 @@ +#![cfg(feature = "test-sbf")] +#![allow(clippy::items_after_test_module)] + +mod program_test; +use { + program_test::TestContext, + solana_program_test::{processor, tokio, ProgramTest}, + solana_sdk::{ + account::Account as SolanaAccount, instruction::InstructionError, pubkey::Pubkey, + signature::Signer, signer::keypair::Keypair, transaction::TransactionError, + transport::TransportError, + }, + spl_token_2022::{extension::BaseStateWithExtensions, processor::Processor}, + spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, + spl_token_group_interface::{ + error::TokenGroupError, instruction::update_group_max_size, state::TokenGroup, + }, + std::{convert::TryInto, sync::Arc}, + test_case::test_case, +}; + +fn setup_program_test() -> ProgramTest { + let mut program_test = ProgramTest::default(); + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(Processor::process), + ); + program_test +} + +async fn setup(mint: Keypair, authority: &Pubkey) -> TestContext { + let program_test = setup_program_test(); + + let context = program_test.start_with_context().await; + let context = Arc::new(tokio::sync::Mutex::new(context)); + let mut context = TestContext { + context, + token_context: None, + }; + let group_address = Some(mint.pubkey()); + context + .init_token_with_mint_keypair_and_freeze_authority( + mint, + vec![ExtensionInitializationParams::GroupPointer { + authority: Some(*authority), + group_address, + }], + None, + ) + .await + .unwrap(); + context +} + +// Successful attempts to set higher than size +#[test_case(0, 0, 10)] +#[test_case(5, 0, 10)] +#[test_case(50, 0, 200_000)] +#[test_case(100_000, 100_000, 200_000)] +#[test_case(50, 0, 300_000_000)] +#[test_case(100_000, 100_000, 300_000_000)] +#[test_case(100_000_000, 100_000_000, 300_000_000)] +#[test_case(0, 0, u32::MAX)] +#[test_case(200_000, 200_000, u32::MAX)] +#[test_case(300_000_000, 300_000_000, u32::MAX)] +// Attempts to set lower than size +#[test_case(5, 5, 4)] +#[test_case(200_000, 200_000, 50)] +#[test_case(200_000, 200_000, 100_000)] +#[test_case(300_000_000, 300_000_000, 50)] +#[test_case(u32::MAX, u32::MAX, 0)] +#[tokio::test] +async fn test_update_group_max_size(max_size: u32, size: u32, new_max_size: u32) { + let authority = Keypair::new(); + let mint_keypair = Keypair::new(); + let mut test_context = setup(mint_keypair.insecure_clone(), &authority.pubkey()).await; + let payer_pubkey = test_context.context.lock().await.payer.pubkey(); + let token_context = test_context.token_context.take().unwrap(); + + let update_authority = Keypair::new(); + let mut token_group = TokenGroup::new( + &mint_keypair.pubkey(), + Some(update_authority.pubkey()).try_into().unwrap(), + max_size, + ); + + token_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &token_context.mint_authority.pubkey(), + &update_authority.pubkey(), + max_size, + &[&token_context.mint_authority], + ) + .await + .unwrap(); + + { + // Update the group's size manually + let mut context = test_context.context.lock().await; + + let group_mint_account = context + .banks_client + .get_account(mint_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + + let old_data = context + .banks_client + .get_account(mint_keypair.pubkey()) + .await + .unwrap() + .unwrap() + .data; + + let data = { + // 0....81: mint + // 82...164: padding + // 165..166: account type + // 167..170: extension discriminator (GroupPointer) + // 171..202: authority + // 203..234: group pointer + // 235..238: extension discriminator (TokenGroup) + // 239..270: mint + // 271..302: update_authority + // 303..306: size + // 307..310: max_size + let (front, back) = old_data.split_at(302); + let (_, back) = back.split_at(4); + let size_bytes = size.to_le_bytes(); + let mut bytes = vec![]; + bytes.extend_from_slice(front); + bytes.extend_from_slice(&size_bytes); + bytes.extend_from_slice(back); + bytes + }; + + context.set_account( + &mint_keypair.pubkey(), + &SolanaAccount { + data, + ..group_mint_account + } + .into(), + ); + + token_group.size = size.into(); + } + + token_group.max_size = new_max_size.into(); + + if new_max_size < size { + let error = token_context + .token + .token_group_update_max_size( + &update_authority.pubkey(), + new_max_size, + &[&update_authority], + ) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenGroupError::SizeExceedsNewMaxSize as u32) + ) + ))), + ); + } else { + token_context + .token + .token_group_update_max_size( + &update_authority.pubkey(), + new_max_size, + &[&update_authority], + ) + .await + .unwrap(); + + let mint_info = token_context.token.get_mint_info().await.unwrap(); + let fetched_group = mint_info.get_extension::().unwrap(); + assert_eq!(fetched_group, &token_group); + } +} + +#[tokio::test] +async fn fail_authority_checks() { + let authority = Keypair::new(); + let mint_keypair = Keypair::new(); + let mut test_context = setup(mint_keypair, &authority.pubkey()).await; + let payer_pubkey = test_context.context.lock().await.payer.pubkey(); + let token_context = test_context.token_context.take().unwrap(); + + let update_authority = Keypair::new(); + token_context + .token + .token_group_initialize_with_rent_transfer( + &payer_pubkey, + &token_context.mint_authority.pubkey(), + &update_authority.pubkey(), + 10, + &[&token_context.mint_authority], + ) + .await + .unwrap(); + + // no signature + let mut instruction = update_group_max_size( + &spl_token_2022::id(), + token_context.token.get_address(), + &update_authority.pubkey(), + 20, + ); + instruction.accounts[1].is_signer = false; + + let error = token_context + .token + .process_ixs(&[instruction], &[] as &[&dyn Signer; 0]) // yuck, but the compiler needs it + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ))) + ); + + // wrong authority + let wrong_authority = Keypair::new(); + let error = token_context + .token + .token_group_update_max_size(&wrong_authority.pubkey(), 20, &[&wrong_authority]) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenGroupError::IncorrectUpdateAuthority as u32) + ) + ))) + ); +} diff --git a/token/program-2022/src/extension/token_group/processor.rs b/token/program-2022/src/extension/token_group/processor.rs index 9966f02ff5e..3032e513b57 100644 --- a/token/program-2022/src/extension/token_group/processor.rs +++ b/token/program-2022/src/extension/token_group/processor.rs @@ -6,7 +6,7 @@ use { error::TokenError, extension::{ alloc_and_serialize, group_pointer::GroupPointer, BaseStateWithExtensions, - StateWithExtensions, + StateWithExtensions, StateWithExtensionsMut, }, state::Mint, }, @@ -18,13 +18,29 @@ use { program_option::COption, pubkey::Pubkey, }, + spl_pod::optional_keys::OptionalNonZeroPubkey, spl_token_group_interface::{ error::TokenGroupError, - instruction::{InitializeGroup, TokenGroupInstruction}, + instruction::{InitializeGroup, TokenGroupInstruction, UpdateGroupMaxSize}, state::TokenGroup, }, }; +fn check_update_authority( + update_authority_info: &AccountInfo, + expected_update_authority: &OptionalNonZeroPubkey, +) -> Result<(), ProgramError> { + if !update_authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + let update_authority = Option::::from(*expected_update_authority) + .ok_or(TokenGroupError::ImmutableGroup)?; + if update_authority != *update_authority_info.key { + return Err(TokenGroupError::IncorrectUpdateAuthority.into()); + } + Ok(()) +} + /// Processes a [InitializeGroup](enum.TokenGroupInstruction.html) instruction. pub fn process_initialize_group( _program_id: &Pubkey, @@ -76,6 +92,30 @@ pub fn process_initialize_group( Ok(()) } +/// Processes an +/// [UpdateGroupMaxSize](enum.GroupInterfaceInstruction.html) +/// instruction +pub fn process_update_group_max_size( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: UpdateGroupMaxSize, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let group_info = next_account_info(account_info_iter)?; + let update_authority_info = next_account_info(account_info_iter)?; + + let mut buffer = group_info.try_borrow_mut_data()?; + let mut state = StateWithExtensionsMut::::unpack(&mut buffer)?; + let group = state.get_extension_mut::()?; + + check_update_authority(update_authority_info, &group.update_authority)?; + + group.update_max_size(data.max_size.into())?; + + Ok(()) +} + /// Processes an [Instruction](enum.Instruction.html). pub fn process_instruction( program_id: &Pubkey, @@ -87,6 +127,10 @@ pub fn process_instruction( msg!("TokenGroupInstruction: InitializeGroup"); process_initialize_group(program_id, accounts, data) } + TokenGroupInstruction::UpdateGroupMaxSize(data) => { + msg!("TokenGroupInstruction: UpdateGroupMaxSize"); + process_update_group_max_size(program_id, accounts, data) + } _ => Err(ProgramError::InvalidInstructionData), } }