From a06dcb208dab5edaf32e904a6bbae96808c05cc7 Mon Sep 17 00:00:00 2001 From: burhankhaja Date: Fri, 14 Nov 2025 17:40:26 +0000 Subject: [PATCH] token-2022: Add group member pointer extension Co-authored-by: M. Daeva --- .../instructions/initialize.rs | 83 ++++++++++ .../group_member_pointer/instructions/mod.rs | 5 + .../instructions/update.rs | 149 ++++++++++++++++++ .../extensions/group_member_pointer/mod.rs | 5 + .../extensions/group_member_pointer/state.rs | 37 +++++ 5 files changed, 279 insertions(+) create mode 100644 programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/initialize.rs create mode 100644 programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/mod.rs create mode 100644 programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/update.rs create mode 100644 programs/token-2022/src/instructions/extensions/group_member_pointer/mod.rs create mode 100644 programs/token-2022/src/instructions/extensions/group_member_pointer/state.rs diff --git a/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/initialize.rs b/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/initialize.rs new file mode 100644 index 00000000..02a7ae49 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/initialize.rs @@ -0,0 +1,83 @@ +use { + crate::instructions::extensions::{ + group_member_pointer::state::{ + offset_group_member_pointer_initialize as OFFSET, + InstructionDiscriminatorGroupMemberPointer, + }, + ExtensionDiscriminator, + }, + pinocchio::{ + account_info::AccountInfo, + cpi::invoke_signed, + instruction::{AccountMeta, Instruction, Signer}, + pubkey::Pubkey, + ProgramResult, + }, +}; + +/// Initialize a new mint with a group member pointer +/// +/// Accounts expected by this instruction: +/// +/// 0. `[writable]` The mint to initialize. +pub struct Initialize<'a, 'b> { + /// Mint Account + pub mint: &'a AccountInfo, + /// Optional authority that can set the member address + pub authority: Option<&'b Pubkey>, + /// Optional account address that holds the member + pub member_address: Option<&'b Pubkey>, + /// Token Program + pub token_program: &'b Pubkey, +} + +impl Initialize<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + #[inline(always)] + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + let account_metas = [AccountMeta::writable(self.mint.key())]; + + let mut buffer = [0u8; OFFSET::END as usize]; + let data = initialize_instruction_data(&mut buffer, self.authority, self.member_address); + + let instruction = Instruction { + program_id: self.token_program, + accounts: &account_metas, + data, + }; + + invoke_signed(&instruction, &[self.mint], signers) + } +} + +#[inline(always)] +fn initialize_instruction_data<'a>( + buffer: &'a mut [u8], + authority: Option<&Pubkey>, + member_address: Option<&Pubkey>, +) -> &'a [u8] { + let mut offset = OFFSET::START as usize; + + // Set discriminators (GroupMemberPointer + Initialize) + buffer[..offset].copy_from_slice(&[ + ExtensionDiscriminator::GroupMemberPointer as u8, + InstructionDiscriminatorGroupMemberPointer::Initialize as u8, + ]); + + // Set authority + if let Some(x) = authority { + buffer[offset..offset + OFFSET::AUTHORITY_PUBKEY as usize].copy_from_slice(x); + } + offset += OFFSET::AUTHORITY_PUBKEY as usize; + + // Set member_address + if let Some(x) = member_address { + buffer[offset..offset + OFFSET::MEMBER_ADDRESS_PUBKEY as usize].copy_from_slice(x); + } + + buffer +} diff --git a/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/mod.rs b/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/mod.rs new file mode 100644 index 00000000..ed7b5260 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/mod.rs @@ -0,0 +1,5 @@ +mod initialize; +mod update; + +pub use initialize::*; +pub use update::*; diff --git a/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/update.rs b/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/update.rs new file mode 100644 index 00000000..6d3d0155 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/group_member_pointer/instructions/update.rs @@ -0,0 +1,149 @@ +use { + crate::{ + instructions::extensions::{ + group_member_pointer::state::{ + offset_group_member_pointer_update as OFFSET, + InstructionDiscriminatorGroupMemberPointer, + }, + ExtensionDiscriminator, + }, + instructions::MAX_MULTISIG_SIGNERS, + }, + core::{mem::MaybeUninit, slice}, + pinocchio::{ + account_info::AccountInfo, + cpi::invoke_signed_with_bounds, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, + ProgramResult, + }, +}; + +/// 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. `[signer]` The group member pointer authority. +/// +/// * Multisignature authority +/// 0. `[writable]` The mint. +/// 1. `[]` The mint's group member pointer authority. +/// 2. `..2+M` `[signer]` M signer accounts. +pub struct Update<'a, 'b> { + /// Mint Account + pub mint: &'a AccountInfo, + /// The group member pointer authority. + pub authority: &'a AccountInfo, + /// The new account address that holds the member + pub member_address: Option<&'b Pubkey>, + /// The Signer accounts if `authority` is a multisig + pub signers: &'a [AccountInfo], + /// Token Program + pub token_program: &'b Pubkey, +} + +impl Update<'_, '_> { + #[inline(always)] + pub fn invoke(&self) -> ProgramResult { + self.invoke_signed(&[]) + } + + #[inline(always)] + pub fn invoke_signed(&self, signers: &[Signer]) -> ProgramResult { + let &Self { + mint, + authority, + signers: account_signers, + token_program, + .. + } = self; + + if account_signers.len() > MAX_MULTISIG_SIGNERS { + Err(ProgramError::InvalidArgument)?; + } + + let num_accounts = 2 + account_signers.len(); + + // Account metadata + const UNINIT_META: MaybeUninit = MaybeUninit::::uninit(); + let mut acc_metas = [UNINIT_META; 2 + MAX_MULTISIG_SIGNERS]; + + unsafe { + // SAFETY: + // - `account_metas` is sized to 2 + MAX_MULTISIG_SIGNERS + // - Index 0 is always present + acc_metas + .get_unchecked_mut(0) + .write(AccountMeta::writable(mint.key())); + // - Index 1 is always present + if account_signers.is_empty() { + acc_metas + .get_unchecked_mut(1) + .write(AccountMeta::readonly_signer(authority.key())); + } else { + acc_metas + .get_unchecked_mut(1) + .write(AccountMeta::readonly(authority.key())); + } + } + + for (account_meta, signer) in acc_metas[2..].iter_mut().zip(account_signers.iter()) { + account_meta.write(AccountMeta::readonly_signer(signer.key())); + } + + let mut buffer = [0u8; OFFSET::END as usize]; + let data = update_instruction_data(&mut buffer, self.member_address); + + let instruction = Instruction { + program_id: token_program, + accounts: unsafe { slice::from_raw_parts(acc_metas.as_ptr() as _, num_accounts) }, + data, + }; + + // Account info array + const UNINIT_INFO: MaybeUninit<&AccountInfo> = MaybeUninit::uninit(); + let mut acc_infos = [UNINIT_INFO; 2 + MAX_MULTISIG_SIGNERS]; + + unsafe { + // SAFETY: + // - `account_infos` is sized to 2 + MAX_MULTISIG_SIGNERS + // - Index 0 is always present + acc_infos.get_unchecked_mut(0).write(mint); + // - Index 1 is always present + acc_infos.get_unchecked_mut(1).write(authority); + } + + // Fill signer accounts + for (account_info, signer) in acc_infos[2..].iter_mut().zip(account_signers.iter()) { + account_info.write(signer); + } + + invoke_signed_with_bounds::<{ 2 + MAX_MULTISIG_SIGNERS }>( + &instruction, + unsafe { slice::from_raw_parts(acc_infos.as_ptr() as _, num_accounts) }, + signers, + ) + } +} + +#[inline(always)] +fn update_instruction_data<'a>(buffer: &'a mut [u8], member_address: Option<&Pubkey>) -> &'a [u8] { + let offset = OFFSET::START as usize; + + // Set discriminators (GroupMemberPointer + Update) + buffer[..offset].copy_from_slice(&[ + ExtensionDiscriminator::GroupMemberPointer as u8, + InstructionDiscriminatorGroupMemberPointer::Update as u8, + ]); + + // Set member_address + if let Some(x) = member_address { + buffer[offset..offset + OFFSET::MEMBER_ADDRESS_PUBKEY as usize].copy_from_slice(x); + } + + buffer +} diff --git a/programs/token-2022/src/instructions/extensions/group_member_pointer/mod.rs b/programs/token-2022/src/instructions/extensions/group_member_pointer/mod.rs new file mode 100644 index 00000000..ec1f45c6 --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/group_member_pointer/mod.rs @@ -0,0 +1,5 @@ +pub mod instructions; +pub mod state; + +pub use instructions::*; +pub use state::*; diff --git a/programs/token-2022/src/instructions/extensions/group_member_pointer/state.rs b/programs/token-2022/src/instructions/extensions/group_member_pointer/state.rs new file mode 100644 index 00000000..b97b610e --- /dev/null +++ b/programs/token-2022/src/instructions/extensions/group_member_pointer/state.rs @@ -0,0 +1,37 @@ +use pinocchio::pubkey::Pubkey; + +#[repr(u8)] +pub enum InstructionDiscriminatorGroupMemberPointer { + Initialize = 0, + Update = 1, +} + +/// Instruction data layout: +/// - [0] : Extension discriminator (1 byte) +/// - [1] : Initialize discriminator (1 byte) +/// - [2..34] : authority pubkey (32 bytes) +/// - [34..66] : member_address pubkey (32 bytes) +pub mod offset_group_member_pointer_initialize { + pub const START: u8 = 2; + pub const AUTHORITY_PUBKEY: u8 = 32; + pub const MEMBER_ADDRESS_PUBKEY: u8 = 32; + pub const END: u8 = START + AUTHORITY_PUBKEY + MEMBER_ADDRESS_PUBKEY; +} + +/// Instruction data layout: +/// - [0]: Extension discriminator (1 byte) +/// - [1]: Instruction discriminator (1 byte) +/// - [2..34]: member_address pubkey (optional, 32 bytes) +pub mod offset_group_member_pointer_update { + pub const START: u8 = 2; + pub const MEMBER_ADDRESS_PUBKEY: u8 = 32; + pub const END: u8 = START + MEMBER_ADDRESS_PUBKEY; +} + +#[repr(C)] +pub struct GroupMemberPointer { + /// Authority that can set the member address + authority: Pubkey, + /// Account address that holds the member + member_address: Pubkey, +}