Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

token 2022: add support for groups #5593

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions token/program-2022/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
13 changes: 13 additions & 0 deletions token/program-2022/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -977,6 +986,8 @@ impl ExtensionType {
pod_get_packed_len::<ConfidentialTransferFeeAmount>()
}
ExtensionType::MetadataPointer => pod_get_packed_len::<MetadataPointer>(),
ExtensionType::TokenGroup => pod_get_packed_len::<TokenGroup>(),
ExtensionType::TokenGroupMember => pod_get_packed_len::<TokenGroupMember>(),
ExtensionType::TokenMetadata => unreachable!(),
#[cfg(test)]
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
Expand Down Expand Up @@ -1037,6 +1048,8 @@ impl ExtensionType {
| ExtensionType::TransferHook
| ExtensionType::ConfidentialTransferFeeConfig
| ExtensionType::MetadataPointer
| ExtensionType::TokenGroup
| ExtensionType::TokenGroupMember
| ExtensionType::TokenMetadata => AccountType::Mint,
ExtensionType::ImmutableOwner
| ExtensionType::TransferFeeAmount
Expand Down
15 changes: 15 additions & 0 deletions token/program-2022/src/extension/token_group/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
}
196 changes: 196 additions & 0 deletions token/program-2022/src/extension/token_group/processor.rs
Original file line number Diff line number Diff line change
@@ -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::<Mint>::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::<TokenGroup>(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::<TokenGroup>()?;

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::<TokenGroup>()?;

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::<Mint>::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::<TokenGroup>()?;

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::<TokenGroupMember>(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)
}
}
}
17 changes: 1 addition & 16 deletions token/program-2022/src/extension/token_metadata/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use {
error::TokenError,
extension::{
alloc_and_serialize, metadata_pointer::MetadataPointer, BaseStateWithExtensions,
StateWithExtensions,
StateWithExtensions, update_authority::check_update_authority
},
state::Mint,
},
Expand All @@ -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::<Pubkey>::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,
Expand Down
26 changes: 26 additions & 0 deletions token/program-2022/src/extension/update_authority.rs
Original file line number Diff line number Diff line change
@@ -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::<Pubkey>::from(*expected_update_authority)
.ok_or(TokenMetadataError::ImmutableMetadata)?;
if update_authority != *update_authority_info.key {
return Err(TokenMetadataError::IncorrectUpdateAuthority.into());
}
Ok(())
}
5 changes: 4 additions & 1 deletion token/program-2022/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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},
};
Expand Down Expand Up @@ -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())
}
Expand Down