diff --git a/Cargo.lock b/Cargo.lock index 4019f31f9e4..ac0ad88f51b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7067,6 +7067,7 @@ dependencies = [ "spl-memo 4.0.0", "spl-pod 0.1.0", "spl-token 4.0.0", + "spl-token-group-interface", "spl-token-metadata-interface 0.2.0", "spl-transfer-hook-interface 0.3.0", "spl-type-length-value 0.3.0", diff --git a/token/program-2022/Cargo.toml b/token/program-2022/Cargo.toml index d1aa8e58dbe..badd7929cdb 100644 --- a/token/program-2022/Cargo.toml +++ b/token/program-2022/Cargo.toml @@ -27,6 +27,7 @@ solana-program = "1.16.16" solana-zk-token-sdk = "1.16.16" spl-memo = { version = "4.0.0", path = "../../memo/program", features = [ "no-entrypoint" ] } spl-token = { version = "4.0", path = "../program", features = ["no-entrypoint"] } +spl-token-group-interface = { version = "0.1.0", path = "../../token-group/interface" } spl-token-metadata-interface = { version = "0.2.0", path = "../../token-metadata/interface" } spl-transfer-hook-interface = { version = "0.3.0", path = "../transfer-hook-interface" } spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value" } diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index 3fc5b04ea43..980cbb18455 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -29,6 +29,7 @@ use { program_error::ProgramError, program_pack::{IsInitialized, Pack}, }, + spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, spl_pod::{ bytemuck::{pod_from_bytes, pod_from_bytes_mut, pod_get_packed_len}, primitives::PodU16, @@ -68,12 +69,16 @@ pub mod non_transferable; pub mod permanent_delegate; /// Utility to reallocate token accounts pub mod reallocate; +/// Token-group extension +pub mod token_group; /// Token-metadata extension pub mod token_metadata; /// Transfer Fee extension pub mod transfer_fee; /// Transfer Hook extension pub mod transfer_hook; +/// Update Authority utility +pub mod update_authority; /// Length in TLV structure #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] @@ -902,6 +907,10 @@ pub enum ExtensionType { ConfidentialTransferFeeAmount, /// Mint contains a pointer to another account (or the same account) that holds metadata MetadataPointer, + /// Mint contains token group configurations + TokenGroup, + /// Mint contains token group _member_ configurations + TokenGroupMember, /// Mint contains token-metadata TokenMetadata, /// Test variable-length mint extension @@ -977,6 +986,8 @@ impl ExtensionType { pod_get_packed_len::() } ExtensionType::MetadataPointer => pod_get_packed_len::(), + ExtensionType::TokenGroup => pod_get_packed_len::(), + ExtensionType::TokenGroupMember => pod_get_packed_len::(), ExtensionType::TokenMetadata => unreachable!(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), @@ -1037,6 +1048,8 @@ impl ExtensionType { | ExtensionType::TransferHook | ExtensionType::ConfidentialTransferFeeConfig | ExtensionType::MetadataPointer + | ExtensionType::TokenGroup + | ExtensionType::TokenGroupMember | ExtensionType::TokenMetadata => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount diff --git a/token/program-2022/src/extension/token_group/mod.rs b/token/program-2022/src/extension/token_group/mod.rs new file mode 100644 index 00000000000..1546ec92975 --- /dev/null +++ b/token/program-2022/src/extension/token_group/mod.rs @@ -0,0 +1,15 @@ +use { + crate::extension::{Extension, ExtensionType}, + spl_token_group_interface::state::{TokenGroup, TokenGroupMember}, +}; + +/// Instruction processor for the TokenGroup extensions +pub mod processor; + +impl Extension for TokenGroup { + const TYPE: ExtensionType = ExtensionType::TokenGroup; +} + +impl Extension for TokenGroupMember { + const TYPE: ExtensionType = ExtensionType::TokenGroupMember; +} diff --git a/token/program-2022/src/extension/token_group/processor.rs b/token/program-2022/src/extension/token_group/processor.rs new file mode 100644 index 00000000000..e6aa7290f53 --- /dev/null +++ b/token/program-2022/src/extension/token_group/processor.rs @@ -0,0 +1,196 @@ +//! Token-group processor + +use { + crate::{ + check_program_account, + error::TokenError, + extension::{ + StateWithExtensions, update_authority::check_update_authority + }, + state::Mint, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_option::COption, + pubkey::Pubkey, + }, + spl_token_group_interface::{ + error::TokenGroupError, + instruction::{ + InitializeGroup, TokenGroupInstruction, UpdateGroupMaxSize, UpdateGroupAuthority + }, + state::{TokenGroup, TokenGroupMember}, + }, + spl_type_length_value::state::TlvStateMut, +}; + +/// Processes a [InitializeGroup](enum.TokenGroupInstruction.html) instruction. +pub fn process_initialize_group( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: InitializeGroup, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let group_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; + let mint_authority_info = next_account_info(account_info_iter)?; + + // check that the mint and group accounts are the same, since the group + // extension should only describe itself + if group_info.key != mint_info.key { + msg!("Group configurations for a mint must be initialized in the mint itself."); + return Err(TokenError::MintMismatch.into()); + } + + // scope the mint authority check, since the mint is in the same account! + { + // This check isn't really needed since we'll be writing into the account, + // but auditors like it + check_program_account(mint_info.owner)?; + let mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + + if !mint_authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if mint.base.mint_authority.as_ref() != COption::Some(mint_authority_info.key) { + return Err(TokenGroupError::IncorrectMintAuthority.into()); + } + } + + // Allocate a TLV entry for the space and write it in + // Assumes that there's enough SOL for the new rent-exemption + let mut buffer = group_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let (group, _) = state.init_value::(false)?; + *group = TokenGroup::new(data.update_authority, data.max_size.into()); + + 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 = TlvStateMut::unpack(&mut buffer)?; + let group = state.get_first_value_mut::()?; + + check_update_authority(update_authority_info, &group.update_authority)?; + + // Update the max size (zero-copy) + group.update_max_size(data.max_size.into())?; + + Ok(()) +} + +/// Processes an +/// [UpdateGroupAuthority](enum.GroupInterfaceInstruction.html) +/// instruction +pub fn process_update_group_authority( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: UpdateGroupAuthority, +) -> 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 = TlvStateMut::unpack(&mut buffer)?; + let mut group = state.get_first_value_mut::()?; + + check_update_authority(update_authority_info, &group.update_authority)?; + + // Update the authority (zero-copy) + group.update_authority = data.new_authority; + + Ok(()) +} + +/// Processes an [InitializeMember](enum.GroupInterfaceInstruction.html) +/// instruction +pub fn process_initialize_group_member( + _program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let member_info = next_account_info(account_info_iter)?; + let member_mint_info = next_account_info(account_info_iter)?; + let member_mint_authority_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)?; + + // scope the mint authority check, since the mint is in the same account! + { + // This check isn't really needed since we'll be writing into the account, + // but auditors like it + check_program_account(member_mint_info.owner)?; + let member_mint_data = member_mint_info.try_borrow_data()?; + let member_mint = StateWithExtensions::::unpack(&member_mint_data)?; + + if !member_mint_authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if member_mint.base.mint_authority.as_ref() != COption::Some(member_mint_authority_info.key) { + return Err(TokenGroupError::IncorrectMintAuthority.into()); + } + } + + // Increment the size of the group + let mut buffer = group_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let group = state.get_first_value_mut::()?; + + check_update_authority( + group_update_authority_info, + &group.update_authority, + )?; + let member_number = group.increment_size()?; + + // Allocate a TLV entry for the space and write it in + let mut buffer = member_info.try_borrow_mut_data()?; + let mut state = TlvStateMut::unpack(&mut buffer)?; + let (member, _) = state.init_value::(false)?; + *member = TokenGroupMember::new(*group_info.key, member_number); + + Ok(()) +} + +/// Processes an [Instruction](enum.Instruction.html). +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction: TokenGroupInstruction, +) -> ProgramResult { + match instruction { + TokenGroupInstruction::InitializeGroup(data) => { + 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) + } + TokenGroupInstruction::UpdateGroupAuthority(data) => { + msg!("TokenGroupInstruction: UpdateGroupAuthority"); + process_update_group_authority(program_id, accounts, data) + } + TokenGroupInstruction::InitializeMember(_) => { + msg!("TokenGroupInstruction: InitializeMember"); + process_initialize_group_member(program_id, accounts) + } + } +} diff --git a/token/program-2022/src/extension/token_metadata/processor.rs b/token/program-2022/src/extension/token_metadata/processor.rs index 88f58d8a9ec..ee06fad2399 100644 --- a/token/program-2022/src/extension/token_metadata/processor.rs +++ b/token/program-2022/src/extension/token_metadata/processor.rs @@ -6,7 +6,7 @@ use { error::TokenError, extension::{ alloc_and_serialize, metadata_pointer::MetadataPointer, BaseStateWithExtensions, - StateWithExtensions, + StateWithExtensions, update_authority::check_update_authority }, state::Mint, }, @@ -29,21 +29,6 @@ use { }, }; -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(TokenMetadataError::ImmutableMetadata)?; - if update_authority != *update_authority_info.key { - return Err(TokenMetadataError::IncorrectUpdateAuthority.into()); - } - Ok(()) -} - /// Processes a [Initialize](enum.TokenMetadataInstruction.html) instruction. pub fn process_initialize( _program_id: &Pubkey, diff --git a/token/program-2022/src/extension/update_authority.rs b/token/program-2022/src/extension/update_authority.rs new file mode 100644 index 00000000000..50875f89e1d --- /dev/null +++ b/token/program-2022/src/extension/update_authority.rs @@ -0,0 +1,26 @@ +//! Utility function for checking an update authority + +use { + solana_program::{ + account_info::AccountInfo, + program_error::ProgramError, + pubkey::Pubkey, + }, + spl_pod::optional_keys::OptionalNonZeroPubkey, + spl_token_metadata_interface::error::TokenMetadataError, +}; + +pub 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(TokenMetadataError::ImmutableMetadata)?; + if update_authority != *update_authority_info.key { + return Err(TokenMetadataError::IncorrectUpdateAuthority.into()); + } + Ok(()) +} \ No newline at end of file diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index 27b58ec3a88..f023d927b3c 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -18,7 +18,7 @@ use { mint_close_authority::MintCloseAuthority, non_transferable::{NonTransferable, NonTransferableAccount}, permanent_delegate::{get_permanent_delegate, PermanentDelegate}, - reallocate, token_metadata, + reallocate, token_group, token_metadata, transfer_fee::{self, TransferFeeAmount, TransferFeeConfig}, transfer_hook::{self, TransferHook, TransferHookAccount}, AccountType, BaseStateWithExtensions, ExtensionType, StateWithExtensions, @@ -41,6 +41,7 @@ use { system_instruction, system_program, sysvar::{rent::Rent, Sysvar}, }, + spl_token_group_interface::instruction::TokenGroupInstruction, spl_token_metadata_interface::instruction::TokenMetadataInstruction, std::convert::{TryFrom, TryInto}, }; @@ -1664,6 +1665,8 @@ impl Processor { } } else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) { token_metadata::processor::process_instruction(program_id, accounts, instruction) + } else if let Ok(instruction) = TokenGroupInstruction::unpack(input) { + token_group::processor::process_instruction(program_id, accounts, instruction) } else { Err(TokenError::InvalidInstruction.into()) }