From 5f3942d3f7709ef4adda821e38e2c65223b465e6 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 22 Oct 2023 13:31:03 +0200 Subject: [PATCH] token 2022: add `GroupMemberPointer` extension --- token/client/src/token.rs | 53 +- .../tests/group_member_pointer.rs | 520 ++++++++++++++++++ .../group_member_pointer/instruction.rs | 149 +++++ .../src/extension/group_member_pointer/mod.rs | 30 + .../group_member_pointer/processor.rs | 167 ++++++ .../src/extension/group_pointer/processor.rs | 6 +- token/program-2022/src/extension/mod.rs | 10 +- token/program-2022/src/instruction.rs | 15 + token/program-2022/src/processor.rs | 21 + 9 files changed, 964 insertions(+), 7 deletions(-) create mode 100644 token/program-2022-test/tests/group_member_pointer.rs create mode 100644 token/program-2022/src/extension/group_member_pointer/instruction.rs create mode 100644 token/program-2022/src/extension/group_member_pointer/mod.rs create mode 100644 token/program-2022/src/extension/group_member_pointer/processor.rs diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 78c29d17cda..5a8a6b47a68 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -42,9 +42,9 @@ use { self, account_info::WithheldTokensInfo, ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, }, - cpi_guard, default_account_state, group_pointer, interest_bearing_mint, memo_transfer, - metadata_pointer, transfer_fee, transfer_hook, BaseStateWithExtensions, Extension, - ExtensionType, StateWithExtensionsOwned, + cpi_guard, default_account_state, group_member_pointer, group_pointer, + interest_bearing_mint, memo_transfer, metadata_pointer, transfer_fee, transfer_hook, + BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsOwned, }, instruction, offchain, proof::ProofLocation, @@ -176,6 +176,12 @@ pub enum ExtensionInitializationParams { authority: Option, group_address: Option, }, + GroupMemberPointer { + authority: Option, + group_update_authority: Pubkey, + group_address: Pubkey, + member_address: Option, + }, } impl ExtensionInitializationParams { /// Get the extension type associated with the init params @@ -194,6 +200,7 @@ impl ExtensionInitializationParams { ExtensionType::ConfidentialTransferFeeConfig } Self::GroupPointer { .. } => ExtensionType::GroupPointer, + Self::GroupMemberPointer { .. } => ExtensionType::GroupMemberPointer, } } /// Generate an appropriate initialization instruction for the given mint @@ -294,6 +301,19 @@ impl ExtensionInitializationParams { authority, group_address, ), + Self::GroupMemberPointer { + authority, + group_update_authority, + group_address, + member_address, + } => group_member_pointer::instruction::initialize( + token_program_id, + mint, + authority, + &group_update_authority, + &group_address, + member_address, + ), } } } @@ -1700,6 +1720,33 @@ where .await } + /// Update group member pointer address + pub async fn update_group_member_address( + &self, + authority: &Pubkey, + group_update_authority: &Pubkey, + group_address: &Pubkey, + new_member_address: Option, + signing_keypairs: &S, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); + + self.process_ixs( + &[group_member_pointer::instruction::update( + &self.program_id, + self.get_address(), + authority, + group_update_authority, + &multisig_signers, + group_address, + new_member_address, + )?], + signing_keypairs, + ) + .await + } + /// Update confidential transfer mint pub async fn confidential_transfer_update_mint( &self, diff --git a/token/program-2022-test/tests/group_member_pointer.rs b/token/program-2022-test/tests/group_member_pointer.rs new file mode 100644 index 00000000000..c43129aaddc --- /dev/null +++ b/token/program-2022-test/tests/group_member_pointer.rs @@ -0,0 +1,520 @@ +#![cfg(feature = "test-sbf")] + +use { + solana_program::system_instruction, solana_program_test::tokio::sync::Mutex, + spl_token_2022::extension::ExtensionType, +}; + +mod program_test; +use { + program_test::{TestContext, TokenContext}, + solana_program_test::{processor, tokio, ProgramTest, ProgramTestContext}, + solana_sdk::{ + account::{Account as SolanaAccount, AccountSharedData}, + instruction::InstructionError, + pubkey::Pubkey, + signature::Signer, + signer::keypair::Keypair, + transaction::{Transaction, TransactionError}, + transport::TransportError, + }, + spl_token_2022::{ + error::TokenError, + extension::{ + group_member_pointer::{ + instruction::{initialize, update}, + GroupMemberPointer, + }, + BaseStateWithExtensions, + }, + instruction, + processor::Processor, + state::Mint, + }, + spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, + std::{convert::TryInto, sync::Arc}, +}; + +fn setup_program_test() -> ProgramTest { + let mut program_test = ProgramTest::default(); + program_test.prefer_bpf(false); + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(Processor::process), + ); + program_test +} + +async fn setup_group_mint( + context: Arc>, + mint: Keypair, + authority: &Pubkey, +) -> TestContext { + 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 +} + +async fn setup_token_group( + token_context: &TokenContext, + mint_authority: &Keypair, + update_authority: &Pubkey, + payer: &Keypair, +) { + token_context + .token + .token_group_initialize_with_rent_transfer( + &payer.pubkey(), + &mint_authority.pubkey(), + update_authority, + 3, + &[&payer, &mint_authority], + ) + .await + .unwrap(); +} + +async fn setup_member_mint( + context: Arc>, + mint: Keypair, + group_address: &Pubkey, + authority: &Pubkey, + group_update_authority: &Keypair, +) -> TestContext { + let mut context = TestContext { + context, + token_context: None, + }; + let member_address = Some(mint.pubkey()); + context + .init_token_with_mint_keypair_and_freeze_authority( + mint, + vec![ExtensionInitializationParams::GroupMemberPointer { + authority: Some(*authority), + group_address: *group_address, + group_update_authority: group_update_authority.pubkey(), + member_address, + }], + None, + &[&group_update_authority], + ) + .await + .unwrap(); + context +} + +#[tokio::test] +async fn success_init() { + let payer = Keypair::new(); + let group_mint = Keypair::new(); + let group_update_authority = Keypair::new(); + let member_mint = Keypair::new(); + let member_authority = Keypair::new(); + + let program_test = setup_program_test(); + let mut context = program_test.start_with_context().await; + context.set_account( + &payer.pubkey(), + &SolanaAccount { + lamports: 500_000_000, + ..SolanaAccount::default() + } + .into(), + ); + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let group_token = setup_group_mint( + context.clone(), + group_mint.insecure_clone(), + &group_update_authority.pubkey(), + ) + .await + .token_context + .take() + .unwrap(); + + setup_token_group( + &group_token, + &group_token.mint_authority, + &group_update_authority.pubkey(), + &payer, + ) + .await; + + let member_token = setup_member_mint( + context, + member_mint.insecure_clone(), + &group_mint.pubkey(), + &member_authority.pubkey(), + &group_update_authority, + ) + .await + .token_context + .take() + .unwrap() + .token; + + let state = member_token.get_mint_info().await.unwrap(); + assert!(state.base.is_initialized); + let extension = state.get_extension::().unwrap(); + assert_eq!( + extension.authority, + Some(member_authority.pubkey()).try_into().unwrap() + ); + assert_eq!(extension.group_address, group_mint.pubkey()); + assert_eq!( + extension.member_address, + Some(member_mint.pubkey()).try_into().unwrap() + ); +} + +#[tokio::test] +async fn fail_init() { + let payer = Keypair::new(); + let group_mint = Keypair::new(); + let group_update_authority = Keypair::new(); + let member_mint = Keypair::new(); + let member_authority = Keypair::new(); + + let program_test = setup_program_test(); + let mut context = program_test.start_with_context().await; + context.set_account( + &payer.pubkey(), + &SolanaAccount { + lamports: 500_000_000, + ..SolanaAccount::default() + } + .into(), + ); + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let group_token = setup_group_mint( + context.clone(), + group_mint.insecure_clone(), + &group_update_authority.pubkey(), + ) + .await + .token_context + .take() + .unwrap(); + + setup_token_group( + &group_token, + &group_token.mint_authority, + &group_update_authority.pubkey(), + &payer, + ) + .await; + + // fail with all none + let mut context = TestContext { + context, + token_context: None, + }; + let err = context + .init_token_with_mint_keypair_and_freeze_authority( + Keypair::new(), + vec![ExtensionInitializationParams::GroupMemberPointer { + authority: None, + group_address: group_mint.pubkey(), + group_update_authority: group_update_authority.pubkey(), + member_address: None, + }], + None, + &[&group_update_authority], + ) + .await + .unwrap_err(); + assert_eq!( + err, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 1, + InstructionError::Custom(TokenError::InvalidInstruction as u32) + ) + ))) + ); + + // fail missing group update authority signature + let mut context = context.context.lock().await; + let space = + ExtensionType::try_calculate_account_len::(&[ExtensionType::GroupMemberPointer]) + .unwrap(); + let lamports = context + .banks_client + .get_rent() + .await + .unwrap() + .minimum_balance(space); + let mut instruction = initialize( + &spl_token_2022::id(), + &member_mint.pubkey(), + Some(member_authority.pubkey()), + &group_update_authority.pubkey(), + &group_mint.pubkey(), + Some(member_mint.pubkey()), + ) + .unwrap(); + instruction.accounts[2].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &member_mint.pubkey(), + lamports, + space as u64, + &spl_token_2022::id(), + ), + instruction, + ], + Some(&payer.pubkey()), + &[&payer, &member_mint], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature,) + ); +} + +#[tokio::test] +async fn success_update() { + let payer = Keypair::new(); + let group_mint = Keypair::new(); + let group_update_authority = Keypair::new(); + let member_mint = Keypair::new(); + let member_authority = Keypair::new(); + + let program_test = setup_program_test(); + let mut context = program_test.start_with_context().await; + context.set_account( + &payer.pubkey(), + &SolanaAccount { + lamports: 500_000_000, + ..SolanaAccount::default() + } + .into(), + ); + let context = Arc::new(tokio::sync::Mutex::new(context)); + + let group_token = setup_group_mint( + context.clone(), + group_mint.insecure_clone(), + &group_update_authority.pubkey(), + ) + .await + .token_context + .take() + .unwrap(); + + setup_token_group( + &group_token, + &group_token.mint_authority, + &group_update_authority.pubkey(), + &payer, + ) + .await; + + let member_token = setup_member_mint( + context, + member_mint.insecure_clone(), + &group_mint.pubkey(), + &member_authority.pubkey(), + &group_update_authority, + ) + .await + .token_context + .take() + .unwrap() + .token; + + let wrong = Keypair::new(); + let new_member_address = Pubkey::default(); + + // fail, wrong signature for group update authority + let err = member_token + .update_group_member_address( + &member_authority.pubkey(), + &wrong.pubkey(), + &group_mint.pubkey(), + Some(new_member_address), + &[&wrong, &member_authority], + ) + .await + .unwrap_err(); + assert_eq!( + err, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::OwnerMismatch as u32) + ) + ))) + ); + + // fail, wrong signature for member pointer authority + let err = member_token + .update_group_member_address( + &wrong.pubkey(), + &group_update_authority.pubkey(), + &group_mint.pubkey(), + Some(new_member_address), + &[&group_update_authority, &wrong], + ) + .await + .unwrap_err(); + assert_eq!( + err, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::OwnerMismatch as u32) + ) + ))) + ); + + let mut context = context.context.lock().await; + + // fail, missing group update authority signature + let mut instruction = update( + &spl_token_2022::id(), + &member_mint.pubkey(), + &member_authority.pubkey(), + &group_update_authority.pubkey(), + &[], + &group_mint.pubkey(), + Some(member_mint.pubkey()), + ) + .unwrap(); + instruction.accounts[3].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &member_mint.pubkey(), + lamports, + space as u64, + &spl_token_2022::id(), + ), + instruction, + ], + Some(&payer.pubkey()), + &[&payer, &member_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature,) + ); + + // fail, missing member pointer authority signature + let mut instruction = update( + &spl_token_2022::id(), + &member_mint.pubkey(), + &member_authority.pubkey(), + &group_update_authority.pubkey(), + &[], + &group_mint.pubkey(), + Some(member_mint.pubkey()), + ) + .unwrap(); + instruction.accounts[2].is_signer = false; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &member_mint.pubkey(), + lamports, + space as u64, + &spl_token_2022::id(), + ), + instruction, + ], + Some(&payer.pubkey()), + &[&payer, &group_update_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError(1, InstructionError::MissingRequiredSignature,) + ); + + // success + member_token + .update_group_member_address( + &member_authority.pubkey(), + &group_update_authority.pubkey(), + &group_mint.pubkey(), + Some(new_member_address), + &[&group_update_authority, &member_authority], + ) + .await + .unwrap(); + let state = member_token.get_mint_info().await.unwrap(); + assert!(state.base.is_initialized); + let extension = state.get_extension::().unwrap(); + assert_eq!( + extension.authority, + Some(member_authority.pubkey()).try_into().unwrap() + ); + assert_eq!(extension.group_address, group_mint.pubkey()); + assert_eq!( + extension.member_address, + Some(new_member_address).try_into().unwrap() + ); + + // set to none + member_token + .update_group_member_address( + &member_authority.pubkey(), + &group_update_authority.pubkey(), + &group_mint.pubkey(), + None, + &[&group_update_authority, &member_authority], + ) + .await + .unwrap(); + let state = member_token.get_mint_info().await.unwrap(); + assert!(state.base.is_initialized); + let extension = state.get_extension::().unwrap(); + assert_eq!( + extension.authority, + Some(member_authority.pubkey()).try_into().unwrap() + ); + assert_eq!(extension.group_address, group_mint.pubkey()); + assert_eq!(extension.member_address, None.try_into().unwrap()); +} diff --git a/token/program-2022/src/extension/group_member_pointer/instruction.rs b/token/program-2022/src/extension/group_member_pointer/instruction.rs new file mode 100644 index 00000000000..b1676353d9e --- /dev/null +++ b/token/program-2022/src/extension/group_member_pointer/instruction.rs @@ -0,0 +1,149 @@ +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + }, + spl_pod::optional_keys::OptionalNonZeroPubkey, + std::convert::TryInto, +}; + +/// Group member pointer extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub enum GroupMemberPointerInstruction { + /// Initialize a new mint with a group member pointer + /// + /// Fails if the mint has already been initialized, so must be called before + /// `InitializeMint`. + /// + /// The mint must have exactly enough space allocated for the base mint (82 + /// bytes), plus 83 bytes of padding, 1 byte reserved for the account type, + /// then space required for this extension, plus any others. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint to initialize. + /// 1. `[]` The group mint. + /// 2. `[signer]` The group's update authority. + /// + /// Data expected by this instruction: + /// `crate::extension::group_member_pointer::instruction::InitializeInstructionData` + Initialize, + /// Update the group member pointer address. Only supported for mints that + /// include the `GroupMemberPointer` extension. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The mint. + /// 1. `[]` The group mint. + /// 2. `[signer]` The group's update authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint. + /// 1. `[]` The group mint. + /// 2. `[]` The mint's group member pointer authority. + /// 3. `[signer]` The group's update authority. + /// 4. ..4+M `[signer]` M signer accounts. + /// + /// Data expected by this instruction: + /// `crate::extension::group_member_pointer::instruction::UpdateInstructionData` + Update, +} + +/// Data expected by `Initialize` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct InitializeInstructionData { + /// The public key for the account that can update the group address + pub authority: OptionalNonZeroPubkey, + /// The account address that holds the group + pub group_address: Pubkey, + /// The account address that holds the member + pub member_address: OptionalNonZeroPubkey, +} + +/// Data expected by `Update` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct UpdateInstructionData { + /// The account address that holds the group + pub group_address: Pubkey, + /// The new account address that holds the group + pub member_address: OptionalNonZeroPubkey, +} + +/// Create an `Initialize` instruction +pub fn initialize( + token_program_id: &Pubkey, + mint: &Pubkey, + authority: Option, + group_update_authority: &Pubkey, + group_address: &Pubkey, + member_address: Option, +) -> Result { + check_program_account(token_program_id)?; + let accounts = vec![ + AccountMeta::new(*mint, false), + AccountMeta::new(*group_address, false), + AccountMeta::new(*group_update_authority, true), + ]; + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::GroupMemberPointerExtension, + GroupMemberPointerInstruction::Initialize, + &InitializeInstructionData { + authority: authority.try_into()?, + group_address: *group_address, + member_address: member_address.try_into()?, + }, + )) +} + +/// Create an `Update` instruction +pub fn update( + token_program_id: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + group_update_authority: &Pubkey, + signers: &[&Pubkey], + group_address: &Pubkey, + member_address: Option, +) -> Result { + check_program_account(token_program_id)?; + let mut accounts = vec![ + AccountMeta::new(*mint, false), + AccountMeta::new(*group_address, false), + AccountMeta::new_readonly(*authority, signers.is_empty()), + AccountMeta::new_readonly(*group_update_authority, true), + ]; + for signer_pubkey in signers.iter() { + accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); + } + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::GroupMemberPointerExtension, + GroupMemberPointerInstruction::Update, + &UpdateInstructionData { + group_address: *group_address, + member_address: member_address.try_into()?, + }, + )) +} diff --git a/token/program-2022/src/extension/group_member_pointer/mod.rs b/token/program-2022/src/extension/group_member_pointer/mod.rs new file mode 100644 index 00000000000..2495fb4f8a9 --- /dev/null +++ b/token/program-2022/src/extension/group_member_pointer/mod.rs @@ -0,0 +1,30 @@ +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; +use { + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + solana_program::pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey, +}; + +/// Instructions for the GroupMemberPointer extension +pub mod instruction; +/// Instruction processor for the GroupMemberPointer extension +pub mod processor; + +/// Group member pointer extension data for mints. +#[repr(C)] +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +pub struct GroupMemberPointer { + /// Authority that can set the group address + pub authority: OptionalNonZeroPubkey, + /// Account address that holds the group + pub group_address: Pubkey, + /// Account address that holds the member + pub member_address: OptionalNonZeroPubkey, +} + +impl Extension for GroupMemberPointer { + const TYPE: ExtensionType = ExtensionType::GroupMemberPointer; +} diff --git a/token/program-2022/src/extension/group_member_pointer/processor.rs b/token/program-2022/src/extension/group_member_pointer/processor.rs new file mode 100644 index 00000000000..f681be767f5 --- /dev/null +++ b/token/program-2022/src/extension/group_member_pointer/processor.rs @@ -0,0 +1,167 @@ +use { + crate::{ + check_program_account, + error::TokenError, + extension::{ + group_member_pointer::{ + instruction::{ + GroupMemberPointerInstruction, InitializeInstructionData, UpdateInstructionData, + }, + GroupMemberPointer, + }, + BaseStateWithExtensions, StateWithExtensions, StateWithExtensionsMut, + }, + instruction::{decode_instruction_data, decode_instruction_type}, + processor::Processor, + state::Mint, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + pubkey::Pubkey, + }, + spl_pod::optional_keys::OptionalNonZeroPubkey, + spl_token_group_interface::state::TokenGroup, +}; + +fn check_group_update_authority( + group_info: &AccountInfo, + group_update_authority_info: &AccountInfo, +) -> Result<(), ProgramError> { + if !group_update_authority_info.is_signer { + msg!("Group update authority must be a signer"); + return Err(ProgramError::MissingRequiredSignature)?; + } + let group_data = group_info.data.borrow(); + let group_mint = StateWithExtensions::::unpack(&group_data)?; + let group_state = group_mint.get_extension::()?; + if Option::::from(group_state.update_authority) + != Some(*group_update_authority_info.key) + { + msg!("Incorrect update authority for group"); + return Err(TokenError::OwnerMismatch)?; + } + Ok(()) +} + +fn process_initialize( + _program_id: &Pubkey, + accounts: &[AccountInfo], + authority: &OptionalNonZeroPubkey, + group_address: &Pubkey, + member_address: &OptionalNonZeroPubkey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + let group_info = next_account_info(account_info_iter)?; + let group_update_authority_info = next_account_info(account_info_iter)?; + + // Group update authority checks + check_group_update_authority(group_info, group_update_authority_info)?; + + let mut mint_data = mint_info.data.borrow_mut(); + let mut mint = StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; + + // Ensure this mint doesn't already have a group member pointer extension + // for this group address + if mint + .get_all_extensions::()? + .iter() + .any(|extension| Option::::from(extension.group_address) == Some(*group_address)) + { + msg!( + "The group member pointer extension for this group address already exists on this \ + mint" + ); + Err(TokenError::InvalidInstruction)?; + } + + if Option::::from(*authority).is_none() + && Option::::from(*member_address).is_none() + { + msg!( + "The group member pointer extension requires at least an authority or an address for \ + initialization, neither was provided" + ); + Err(TokenError::InvalidInstruction)?; + } + + let extension = mint.init_extension_allow_repeating::()?; + extension.authority = *authority; + extension.group_address = *group_address; + extension.member_address = *member_address; + Ok(()) +} + +fn process_update( + program_id: &Pubkey, + accounts: &[AccountInfo], + group_address: &Pubkey, + new_member_address: &OptionalNonZeroPubkey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_info = next_account_info(account_info_iter)?; + let group_info = next_account_info(account_info_iter)?; + let owner_info = next_account_info(account_info_iter)?; + let group_update_authority_info = next_account_info(account_info_iter)?; + + // Group update authority checks + check_group_update_authority(group_info, group_update_authority_info)?; + + let mut mint_data = mint_info.data.borrow_mut(); + let mut mint = StateWithExtensionsMut::::unpack(&mut mint_data)?; + + let extension = + mint.get_first_matched_repeating_extension_mut::(|extension| { + extension.group_address == *group_address + })?; + let authority = + Option::::from(extension.authority).ok_or(TokenError::NoAuthorityExists)?; + + let owner_info_data_len = owner_info.data_len(); + Processor::validate_owner( + program_id, + &authority, + owner_info, + owner_info_data_len, + account_info_iter.as_slice(), + )?; + + extension.member_address = *new_member_address; + Ok(()) +} + +pub(crate) fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + check_program_account(program_id)?; + match decode_instruction_type(input)? { + GroupMemberPointerInstruction::Initialize => { + msg!("GroupMemberPointerInstruction::Initialize"); + let InitializeInstructionData { + authority, + group_address, + member_address, + } = decode_instruction_data(input)?; + process_initialize( + program_id, + accounts, + authority, + group_address, + member_address, + ) + } + GroupMemberPointerInstruction::Update => { + msg!("GroupMemberPointerInstruction::Update"); + let UpdateInstructionData { + group_address, + member_address, + } = decode_instruction_data(input)?; + process_update(program_id, accounts, group_address, member_address) + } + } +} diff --git a/token/program-2022/src/extension/group_pointer/processor.rs b/token/program-2022/src/extension/group_pointer/processor.rs index 7c99afc0001..07a8c96766e 100644 --- a/token/program-2022/src/extension/group_pointer/processor.rs +++ b/token/program-2022/src/extension/group_pointer/processor.rs @@ -35,9 +35,6 @@ fn process_initialize( let mut mint_data = mint_account_info.data.borrow_mut(); let mut mint = StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; - let extension = mint.init_extension::(true)?; - extension.authority = *authority; - if Option::::from(*authority).is_none() && Option::::from(*group_address).is_none() { @@ -47,6 +44,9 @@ fn process_initialize( ); Err(TokenError::InvalidInstruction)?; } + + let extension = mint.init_extension::(true)?; + extension.authority = *authority; extension.group_address = *group_address; Ok(()) } diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index ddbc827e188..2ea52e2e556 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -12,6 +12,7 @@ use { }, cpi_guard::CpiGuard, default_account_state::DefaultAccountState, + group_member_pointer::GroupMemberPointer, group_pointer::GroupPointer, immutable_owner::ImmutableOwner, interest_bearing_mint::InterestBearingConfig, @@ -53,6 +54,8 @@ pub mod confidential_transfer_fee; pub mod cpi_guard; /// Default Account State extension pub mod default_account_state; +/// Group Member Pointer extension +pub mod group_member_pointer; /// Group Pointer extension pub mod group_pointer; /// Immutable Owner extension @@ -1164,6 +1167,9 @@ pub enum ExtensionType { GroupPointer, /// Mint contains token group configurations TokenGroup, + /// Mint contains a pointer to another account (or the same account) that + /// holds group member configurations + GroupMemberPointer, /// Test variable-length mint extension #[cfg(test)] VariableLenMintTest = u16::MAX - 2, @@ -1242,6 +1248,7 @@ impl ExtensionType { ExtensionType::TokenMetadata => unreachable!(), ExtensionType::GroupPointer => pod_get_packed_len::(), ExtensionType::TokenGroup => pod_get_packed_len::(), + ExtensionType::GroupMemberPointer => pod_get_packed_len::(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1303,7 +1310,8 @@ impl ExtensionType { | ExtensionType::MetadataPointer | ExtensionType::TokenMetadata | ExtensionType::GroupPointer - | ExtensionType::TokenGroup => AccountType::Mint, + | ExtensionType::TokenGroup + | ExtensionType::GroupMemberPointer => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index a34f4e31bcb..99861a75cd1 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -693,6 +693,13 @@ pub enum TokenInstruction<'a> { /// for further details about the extended instructions that share this /// instruction prefix GroupPointerExtension, + /// The common instruction prefix for group member pointer extension + /// instructions. + /// + /// See `extension::group_member_pointer::instruction::GroupMemberPointerInstruction` + /// for further details about the extended instructions that share this + /// instruction prefix + GroupMemberPointerExtension, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a @@ -834,6 +841,7 @@ impl<'a> TokenInstruction<'a> { 38 => Self::WithdrawExcessLamports, 39 => Self::MetadataPointerExtension, 40 => Self::GroupPointerExtension, + 41 => Self::GroupMemberPointerExtension, _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -1003,6 +1011,9 @@ impl<'a> TokenInstruction<'a> { &Self::GroupPointerExtension => { buf.push(40); } + &Self::GroupMemberPointerExtension => { + buf.push(41); + } }; buf } @@ -1098,6 +1109,8 @@ pub enum AuthorityType { MetadataPointer, /// Authority to set the group address GroupPointer, + /// Authority to set the group member address + GroupMemberPointer, } impl AuthorityType { @@ -1117,6 +1130,7 @@ impl AuthorityType { AuthorityType::ConfidentialTransferFeeConfig => 11, AuthorityType::MetadataPointer => 12, AuthorityType::GroupPointer => 13, + AuthorityType::GroupMemberPointer => 14, } } @@ -1136,6 +1150,7 @@ impl AuthorityType { 11 => Ok(AuthorityType::ConfidentialTransferFeeConfig), 12 => Ok(AuthorityType::MetadataPointer), 13 => Ok(AuthorityType::GroupPointer), + 14 => Ok(AuthorityType::GroupMemberPointer), _ => Err(TokenError::InvalidInstruction.into()), } } diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index 95068c3a0ba..316631e816e 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -11,6 +11,7 @@ use { }, cpi_guard::{self, in_cpi, CpiGuard}, default_account_state::{self, DefaultAccountState}, + group_member_pointer::{self, GroupMemberPointer}, group_pointer::{self, GroupPointer}, immutable_owner::ImmutableOwner, interest_bearing_mint::{self, InterestBearingConfig}, @@ -866,6 +867,19 @@ impl Processor { )?; extension.authority = new_authority.try_into()?; } + AuthorityType::GroupMemberPointer => { + let extension = mint.get_extension_mut::()?; + let maybe_authority: Option = extension.authority.into(); + let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; + Self::validate_owner( + program_id, + &authority, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )?; + extension.authority = new_authority.try_into()?; + } _ => { return Err(TokenError::AuthorityTypeNotSupported.into()); } @@ -1688,6 +1702,13 @@ impl Processor { TokenInstruction::GroupPointerExtension => { group_pointer::processor::process_instruction(program_id, accounts, &input[1..]) } + TokenInstruction::GroupMemberPointerExtension => { + group_member_pointer::processor::process_instruction( + program_id, + accounts, + &input[1..], + ) + } } } else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) { token_metadata::processor::process_instruction(program_id, accounts, instruction)