diff --git a/interface/src/extension/mod.rs b/interface/src/extension/mod.rs index 232556803..4065e1446 100644 --- a/interface/src/extension/mod.rs +++ b/interface/src/extension/mod.rs @@ -23,6 +23,7 @@ use { non_transferable::{NonTransferable, NonTransferableAccount}, pausable::{PausableAccount, PausableConfig}, permanent_delegate::PermanentDelegate, + permissioned_burn::PermissionedBurnConfig, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, transfer_hook::{TransferHook, TransferHookAccount}, @@ -76,6 +77,8 @@ pub mod non_transferable; pub mod pausable; /// Permanent Delegate extension pub mod permanent_delegate; +/// Permissioned burn extension +pub mod permissioned_burn; /// Scaled UI Amount extension pub mod scaled_ui_amount; /// Token-group extension @@ -1119,6 +1122,8 @@ pub enum ExtensionType { Pausable, /// Indicates that the account belongs to a pausable mint PausableAccount, + /// Tokens burning requires approval from authorirty. + PermissionedBurn, /// Test variable-length mint extension #[cfg(test)] @@ -1204,6 +1209,7 @@ impl ExtensionType { ExtensionType::ScaledUiAmount => pod_get_packed_len::(), ExtensionType::Pausable => pod_get_packed_len::(), ExtensionType::PausableAccount => pod_get_packed_len::(), + ExtensionType::PermissionedBurn => pod_get_packed_len::(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1270,7 +1276,8 @@ impl ExtensionType { | ExtensionType::ConfidentialMintBurn | ExtensionType::TokenGroupMember | ExtensionType::ScaledUiAmount - | ExtensionType::Pausable => AccountType::Mint, + | ExtensionType::Pausable + | ExtensionType::PermissionedBurn => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount diff --git a/interface/src/extension/permissioned_burn/instruction.rs b/interface/src/extension/permissioned_burn/instruction.rs new file mode 100644 index 000000000..c46143a20 --- /dev/null +++ b/interface/src/extension/permissioned_burn/instruction.rs @@ -0,0 +1,59 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_instruction::{AccountMeta, Instruction}, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, +}; + +/// Permissioned Burn extension instructions +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub enum PermissionedBurnInstruction { + /// Require permissioned burn for the given mint account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint account to initialize. + /// + /// Data expected by this instruction: + /// `crate::extension::permissioned_burn::instruction::InitializeInstructionData` + Initialize, +} + +/// Data expected by `PermissionedBurnInstruction::Initialize` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct InitializeInstructionData { + /// The public key for the account that is required for token burning. + pub authority: Pubkey, +} + +/// Create an `Initialize` instruction +pub fn initialize( + token_program_id: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, +) -> Result { + check_program_account(token_program_id)?; + let accounts = vec![AccountMeta::new(*mint, false)]; + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::PermissionedBurnExtension, + PermissionedBurnInstruction::Initialize, + &InitializeInstructionData { + authority: *authority, + }, + )) +} diff --git a/interface/src/extension/permissioned_burn/mod.rs b/interface/src/extension/permissioned_burn/mod.rs new file mode 100644 index 000000000..00e42831e --- /dev/null +++ b/interface/src/extension/permissioned_burn/mod.rs @@ -0,0 +1,24 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + spl_pod::optional_keys::OptionalNonZeroPubkey +}; + +/// Instruction types for the permissioned burn extension +pub mod instruction; + +/// Indicates that the tokens from this mint require permissioned burn +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct PermissionedBurnConfig { + /// Authority that is required for burning + pub authority: OptionalNonZeroPubkey, +} + +impl Extension for PermissionedBurnConfig { + const TYPE: ExtensionType = ExtensionType::PermissionedBurn; +} diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index cabd7ce56..fcb938ba0 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -731,6 +731,8 @@ pub enum TokenInstruction<'a> { ScaledUiAmountExtension, /// Instruction prefix for instructions to the pausable extension PausableExtension, + /// Instruction prefix for instructions to the permissioned burn extension + PermissionedBurnExtension, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a @@ -873,6 +875,7 @@ impl<'a> TokenInstruction<'a> { 42 => Self::ConfidentialMintBurnExtension, 43 => Self::ScaledUiAmountExtension, 44 => Self::PausableExtension, + 46 => Self::PermissionedBurnExtension, _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -1053,6 +1056,9 @@ impl<'a> TokenInstruction<'a> { &Self::PausableExtension => { buf.push(44); } + &Self::PermissionedBurnExtension => { + buf.push(46); + } }; buf } @@ -1154,6 +1160,8 @@ pub enum AuthorityType { ScaledUiAmount, /// Authority to pause or resume minting / transferring / burning Pause, + /// Authority to perform a permissioned token burn + PermissionedBurn, } impl AuthorityType { @@ -1176,6 +1184,7 @@ impl AuthorityType { AuthorityType::GroupMemberPointer => 14, AuthorityType::ScaledUiAmount => 15, AuthorityType::Pause => 16, + AuthorityType::PermissionedBurn => 17, } } @@ -1199,6 +1208,7 @@ impl AuthorityType { 14 => Ok(AuthorityType::GroupMemberPointer), 15 => Ok(AuthorityType::ScaledUiAmount), 16 => Ok(AuthorityType::Pause), + 17 => Ok(AuthorityType::PermissionedBurn), _ => Err(TokenError::InvalidInstruction.into()), } } diff --git a/program/src/extension/mod.rs b/program/src/extension/mod.rs index 89be5eb39..aadb95847 100644 --- a/program/src/extension/mod.rs +++ b/program/src/extension/mod.rs @@ -28,6 +28,8 @@ pub mod non_transferable; pub mod pausable; /// Permanent Delegate extension pub mod permanent_delegate; +/// Permissioned burn extension +pub mod permissioned_burn; /// Utility to reallocate token accounts pub mod reallocate; /// Scaled UI Amount extension diff --git a/program/src/extension/permissioned_burn/instruction.rs b/program/src/extension/permissioned_burn/instruction.rs new file mode 100644 index 000000000..d93ad582d --- /dev/null +++ b/program/src/extension/permissioned_burn/instruction.rs @@ -0,0 +1,5 @@ +#![deprecated( + since = "9.1.0", + note = "Use spl_token_2022_interface instead and remove spl_token_2022 as a dependency" +)] +pub use spl_token_2022_interface::extension::permissioned_burn::instruction::*; diff --git a/program/src/extension/permissioned_burn/mod.rs b/program/src/extension/permissioned_burn/mod.rs new file mode 100644 index 000000000..cf8bbbfcd --- /dev/null +++ b/program/src/extension/permissioned_burn/mod.rs @@ -0,0 +1,10 @@ +/// Instruction types for the permissioned burn extension +pub mod instruction; +/// Instruction processor for the permissioned burn extension +pub mod processor; + +#[deprecated( + since = "9.1.0", + note = "Use spl_token_2022_interface instead and remove spl_token_2022 as a dependency" +)] +pub use spl_token_2022_interface::extension::permissioned_burn::PermissionedBurnConfig; diff --git a/program/src/extension/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs new file mode 100644 index 000000000..50f3bb838 --- /dev/null +++ b/program/src/extension/permissioned_burn/processor.rs @@ -0,0 +1,50 @@ +use { + solana_account_info::{next_account_info, AccountInfo}, + solana_msg::msg, + solana_program_error::ProgramResult, + solana_pubkey::Pubkey, + spl_token_2022_interface::{ + check_program_account, + extension::{ + permissioned_burn::{ + instruction::{InitializeInstructionData, PermissionedBurnInstruction}, + PermissionedBurnConfig, + }, + BaseStateWithExtensionsMut, PodStateWithExtensionsMut, + }, + instruction::{decode_instruction_data, decode_instruction_type}, + pod::PodMint, + }, +}; + +fn process_initialize( + _program_id: &Pubkey, + accounts: &[AccountInfo], + authority: &Pubkey, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_account_info = next_account_info(account_info_iter)?; + let mut mint_data = mint_account_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; + + let extension = mint.init_extension::(true)?; + extension.authority = Some(*authority).try_into()?; + + Ok(()) +} + +pub(crate) fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + check_program_account(program_id)?; + + match decode_instruction_type(input)? { + PermissionedBurnInstruction::Initialize => { + msg!("PermissionedBurnInstruction::Initialize"); + let InitializeInstructionData { authority } = decode_instruction_data(input)?; + process_initialize(program_id, accounts, authority) + } + } +} diff --git a/program/src/pod_instruction.rs b/program/src/pod_instruction.rs index 308c0cd04..9dcaaa5cb 100644 --- a/program/src/pod_instruction.rs +++ b/program/src/pod_instruction.rs @@ -115,6 +115,7 @@ pub(crate) enum PodTokenInstruction { ConfidentialMintBurnExtension, ScaledUiAmountExtension, PausableExtension, + PermissionedBurnExtension, } fn unpack_pubkey_option(input: &[u8]) -> Result, ProgramError> { diff --git a/program/src/processor.rs b/program/src/processor.rs index 479621926..feec9dcd4 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -7,8 +7,8 @@ use { cpi_guard::{self, in_cpi}, default_account_state, group_member_pointer, group_pointer, interest_bearing_mint, memo_transfer::{self, check_previous_sibling_instruction_is_memo}, - metadata_pointer, pausable, reallocate, scaled_ui_amount, token_group, token_metadata, - transfer_fee, transfer_hook, + metadata_pointer, pausable, permissioned_burn, reallocate, scaled_ui_amount, + token_group, token_metadata, transfer_fee, transfer_hook, }, pod_instruction::{ decode_instruction_data_with_coption_pubkey, AmountCheckedData, AmountData, @@ -51,6 +51,7 @@ use { non_transferable::{NonTransferable, NonTransferableAccount}, pausable::{PausableAccount, PausableConfig}, permanent_delegate::{get_permanent_delegate, PermanentDelegate}, + permissioned_burn::PermissionedBurnConfig, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, transfer_hook::{TransferHook, TransferHookAccount}, @@ -966,6 +967,19 @@ impl Processor { )?; extension.authority = new_authority.try_into()?; } + AuthorityType::PermissionedBurn => { + 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()); } @@ -1109,6 +1123,20 @@ impl Processor { return Err(TokenError::MintPaused.into()); } } + if let Ok(ext) = mint.get_extension::() { + // Pull the required extra signer from the accounts + let approver_ai = next_account_info(account_info_iter)?; + + if !approver_ai.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + let maybe_burn_authority: Option = ext.authority.into(); + if Some(*approver_ai.key) != maybe_burn_authority { + return Err(ProgramError::InvalidAccountData); + } + } + let maybe_permanent_delegate = get_permanent_delegate(&mint); if let Ok(cpi_guard) = source_account.get_extension::() { @@ -1942,6 +1970,14 @@ impl Processor { msg!("Instruction: PausableExtension"); pausable::processor::process_instruction(program_id, accounts, &input[1..]) } + PodTokenInstruction::PermissionedBurnExtension => { + msg!("Instruction: PermissionedBurnExtension"); + permissioned_burn::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)