Repository: solana-labs/solana-program-library
Pull request #5845 (2023-11-15 13:21:08)

ci: Update actions/setup-python to v4
#### Problem

As a final follow-up to #5844, actions/setup-python is also outdated.

#### Solution

To keep this PR slim, just update the stake pool job to use actions/setup-python v4


Patch:
diff --git a/.github/workflows/pull-request-stake-pool.yml b/.github/workflows/pull-request-stake-pool.yml
index 7802eacdd..9d8a3e21d 100644
--- a/.github/workflows/pull-request-stake-pool.yml
+++ b/.github/workflows/pull-request-stake-pool.yml
@@ -107,7 +107,7 @@ jobs:
       - uses: actions/checkout@v4
 
       - name: Setup Python version
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: 3.8
 
diff --git a/token/client/src/token.rs b/token/client/src/token.rs
index 4af5eaa46..a1383cfb3 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<S: Signers>(
+        &self,
+        update_authority: &Pubkey,
+        new_max_size: u32,
+        signing_keypairs: &S,
+    ) -> TokenResult<T::Output> {
+        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/src/extension/token_group/processor.rs b/token/program-2022/src/extension/token_group/processor.rs
index 9966f02ff..3032e513b 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::<Pubkey>::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::<Mint>::unpack(&mut buffer)?;
+    let group = state.get_extension_mut::<TokenGroup>()?;
+
+    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),
     }
 }


Test patch:
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 000000000..9e8382fae
--- /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::<TokenGroup>().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)
+            )
+        )))
+    );
+}