From 905f41e0feaa8fa35b6cffae98c07571ce114b9b Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 31 Oct 2025 17:44:10 +0100 Subject: [PATCH 01/32] Permissioned Burn Extension --- .../permissioned_burn/instruction.rs | 88 ++++++++++++++++++ .../src/extension/permissioned_burn/mod.rs | 37 ++++++++ program/src/extension/mod.rs | 2 + .../permissioned_burn/instruction.rs | 5 ++ .../src/extension/permissioned_burn/mod.rs | 10 +++ .../extension/permissioned_burn/processor.rs | 89 +++++++++++++++++++ program/src/pod_instruction.rs | 1 + program/src/processor.rs | 6 +- 8 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 interface/src/extension/permissioned_burn/instruction.rs create mode 100644 interface/src/extension/permissioned_burn/mod.rs create mode 100644 program/src/extension/permissioned_burn/instruction.rs create mode 100644 program/src/extension/permissioned_burn/mod.rs create mode 100644 program/src/extension/permissioned_burn/processor.rs diff --git a/interface/src/extension/permissioned_burn/instruction.rs b/interface/src/extension/permissioned_burn/instruction.rs new file mode 100644 index 000000000..6569aec69 --- /dev/null +++ b/interface/src/extension/permissioned_burn/instruction.rs @@ -0,0 +1,88 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + 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 for which to enable. + /// + /// Data expected by this instruction: + /// `crate::extension::permissioned_burn::instruction::EnableInstructionData` + Enable, + /// Stop requiring burn to be signed by an additional authority. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint account for which to enable. + Disable, +} + +/// Data expected by `PausableInstruction::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 EnableInstructionData { + /// The public key for the account that is required for token burning. + pub authority: Pubkey, +} + +/// Create an `Enable` instruction +pub fn enable( + 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, + PausableInstruction::Enable, + &InitializeInstructionData { + authority: *authority, + }, + )) +} + +/// Create a `Disable` instruction +pub fn disable( + token_program_id: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + signers: &[&Pubkey], +) -> Result { + check_program_account(token_program_id)?; + let mut accounts = vec![ + AccountMeta::new(*mint, false), + AccountMeta::new_readonly(*authority, signers.is_empty()), + ]; + for signer_pubkey in signers.iter() { + accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); + } + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::PermissionedBurnExtension, + PausableInstruction::Disable, + &(), + )) +} diff --git a/interface/src/extension/permissioned_burn/mod.rs b/interface/src/extension/permissioned_burn/mod.rs new file mode 100644 index 000000000..8bc07eecf --- /dev/null +++ b/interface/src/extension/permissioned_burn/mod.rs @@ -0,0 +1,37 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use { + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}, +}; + +/// 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, + /// Whether permission from the authority is required to burn + pub enabled: PodBool, +} + +/// Indicates that the tokens from this account belong to a permissioned burn mint +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PermissionedBurnAccount; + +impl Extension for PermissionedBurnConfig { + const TYPE: ExtensionType = ExtensionType::PermissionedBurn; +} + +impl Extension for PermissionedBurnAccount { + const TYPE: ExtensionType = ExtensionType::PermissionedBurn; +} diff --git a/program/src/extension/mod.rs b/program/src/extension/mod.rs index 89be5eb39..95ba3fa07 100644 --- a/program/src/extension/mod.rs +++ b/program/src/extension/mod.rs @@ -26,6 +26,8 @@ pub mod mint_close_authority; pub mod non_transferable; /// Pausable extension pub mod pausable; +/// Pausable extension +pub mod permissioned_burn; /// Permanent Delegate extension pub mod permanent_delegate; /// Utility to reallocate token accounts diff --git a/program/src/extension/permissioned_burn/instruction.rs b/program/src/extension/permissioned_burn/instruction.rs new file mode 100644 index 000000000..c99bc1367 --- /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::pausable::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..7d8fbe6e5 --- /dev/null +++ b/program/src/extension/permissioned_burn/mod.rs @@ -0,0 +1,10 @@ +/// Instruction types for the pausable extension +pub mod instruction; +/// Instruction processor for the pausable 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::pausable::{PausableAccount, PausableConfig}; diff --git a/program/src/extension/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs new file mode 100644 index 000000000..f6867eab6 --- /dev/null +++ b/program/src/extension/permissioned_burn/processor.rs @@ -0,0 +1,89 @@ +use { + crate::processor::Processor, + solana_account_info::{next_account_info, AccountInfo}, + solana_msg::msg, + solana_program_error::ProgramResult, + solana_pubkey::Pubkey, + spl_token_2022_interface::{ + check_program_account, + error::TokenError, + extension::{ + pausable::{ + instruction::{InitializeInstructionData, PausableInstruction}, + PausableConfig, + }, + 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(()) +} + +/// Pause or resume minting / burning / transferring on the mint +fn process_toggle_pause( + program_id: &Pubkey, + accounts: &[AccountInfo], + pause: bool, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let mint_account_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let authority_info_data_len = authority_info.data_len(); + + let mut mint_data = mint_account_info.data.borrow_mut(); + let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; + let extension = mint.get_extension_mut::()?; + let maybe_authority: Option = extension.authority.into(); + let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; + + Processor::validate_owner( + program_id, + &authority, + authority_info, + authority_info_data_len, + account_info_iter.as_slice(), + )?; + + extension.paused = pause.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)? { + PausableInstruction::Initialize => { + msg!("PausableInstruction::Initialize"); + let InitializeInstructionData { authority } = decode_instruction_data(input)?; + process_initialize(program_id, accounts, authority) + } + PausableInstruction::Pause => { + msg!("PausableInstruction::Pause"); + process_toggle_pause(program_id, accounts, true /* pause */) + } + PausableInstruction::Resume => { + msg!("PausableInstruction::Resume"); + process_toggle_pause(program_id, accounts, false /* resume */) + } + } +} 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..853da8486 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -8,7 +8,7 @@ use { 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, + transfer_fee, transfer_hook, permissioned_burn, }, pod_instruction::{ decode_instruction_data_with_coption_pubkey, AmountCheckedData, AmountData, @@ -1942,6 +1942,10 @@ 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) From f0175dcd48b402486a4e67c713efb44d6ecf9baa Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 31 Oct 2025 17:58:24 +0100 Subject: [PATCH 02/32] compiles --- interface/src/extension/mod.rs | 11 +++++++++++ .../src/extension/permissioned_burn/instruction.rs | 9 +++++---- interface/src/instruction.rs | 5 +++++ .../src/extension/permissioned_burn/instruction.rs | 2 +- program/src/extension/permissioned_burn/mod.rs | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/interface/src/extension/mod.rs b/interface/src/extension/mod.rs index 232556803..529b73a3b 100644 --- a/interface/src/extension/mod.rs +++ b/interface/src/extension/mod.rs @@ -22,6 +22,7 @@ use { mint_close_authority::MintCloseAuthority, non_transferable::{NonTransferable, NonTransferableAccount}, pausable::{PausableAccount, PausableConfig}, + permissioned_burn::{PermissionedBurnAccount, PermissionedBurnConfig}, permanent_delegate::PermanentDelegate, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, @@ -74,6 +75,8 @@ pub mod mint_close_authority; pub mod non_transferable; /// Pausable extension pub mod pausable; +/// Permissioned burn extension +pub mod permissioned_burn; /// Permanent Delegate extension pub mod permanent_delegate; /// Scaled UI Amount extension @@ -1119,6 +1122,10 @@ pub enum ExtensionType { Pausable, /// Indicates that the account belongs to a pausable mint PausableAccount, + /// Tokens burning requires approval from authorirty. + PermissionedBurn, + /// Indicates that the account belongs to a mint requiring permissioned burn. + PermissionedBurnAccount, /// Test variable-length mint extension #[cfg(test)] @@ -1204,6 +1211,8 @@ 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::(), + ExtensionType::PermissionedBurnAccount => pod_get_packed_len::(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1271,6 +1280,7 @@ impl ExtensionType { | ExtensionType::TokenGroupMember | ExtensionType::ScaledUiAmount | ExtensionType::Pausable => AccountType::Mint, + | ExtensionType::PermissionedBurn => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount @@ -1280,6 +1290,7 @@ impl ExtensionType { | ExtensionType::CpiGuard | ExtensionType::ConfidentialTransferFeeAmount | ExtensionType::PausableAccount => AccountType::Account, + | ExtensionType::PermissionedBurnAccount => AccountType::Account, #[cfg(test)] ExtensionType::VariableLenMintTest => AccountType::Mint, #[cfg(test)] diff --git a/interface/src/extension/permissioned_burn/instruction.rs b/interface/src/extension/permissioned_burn/instruction.rs index 6569aec69..6850c6fb6 100644 --- a/interface/src/extension/permissioned_burn/instruction.rs +++ b/interface/src/extension/permissioned_burn/instruction.rs @@ -5,6 +5,7 @@ use { check_program_account, instruction::{encode_instruction, TokenInstruction}, }, + bytemuck::{Pod, Zeroable}, num_enum::{IntoPrimitive, TryFromPrimitive}, solana_instruction::{AccountMeta, Instruction}, solana_program_error::ProgramError, @@ -34,7 +35,7 @@ pub enum PermissionedBurnInstruction { Disable, } -/// Data expected by `PausableInstruction::Initialize` +/// Data expected by `PermissionedBurnInstruction::Enable` #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] #[derive(Clone, Copy, Pod, Zeroable)] @@ -56,8 +57,8 @@ pub fn enable( token_program_id, accounts, TokenInstruction::PermissionedBurnExtension, - PausableInstruction::Enable, - &InitializeInstructionData { + PermissionedBurnInstruction::Enable, + &EnableInstructionData { authority: *authority, }, )) @@ -82,7 +83,7 @@ pub fn disable( token_program_id, accounts, TokenInstruction::PermissionedBurnExtension, - PausableInstruction::Disable, + PermissionedBurnInstruction::Disable, &(), )) } diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index cabd7ce56..d80cee8d4 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 @@ -1053,6 +1055,9 @@ impl<'a> TokenInstruction<'a> { &Self::PausableExtension => { buf.push(44); } + &Self::PermissionedBurnExtension => { + buf.push(45); + } }; buf } diff --git a/program/src/extension/permissioned_burn/instruction.rs b/program/src/extension/permissioned_burn/instruction.rs index c99bc1367..d93ad582d 100644 --- a/program/src/extension/permissioned_burn/instruction.rs +++ b/program/src/extension/permissioned_burn/instruction.rs @@ -2,4 +2,4 @@ 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::pausable::instruction::*; +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 index 7d8fbe6e5..58e2a3946 100644 --- a/program/src/extension/permissioned_burn/mod.rs +++ b/program/src/extension/permissioned_burn/mod.rs @@ -7,4 +7,4 @@ pub mod processor; 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::pausable::{PausableAccount, PausableConfig}; +pub use spl_token_2022_interface::extension::permissioned_burn::{PermissionedBurnAccount, PermissionedBurnConfig}; From ea637bd6628c2025818ec77a8063a224b9e4eb5b Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 31 Oct 2025 19:37:31 +0100 Subject: [PATCH 03/32] handle permissioned burn in processor --- .../src/extension/permissioned_burn/mod.rs | 2 +- .../src/extension/permissioned_burn/mod.rs | 4 +- .../extension/permissioned_burn/processor.rs | 79 +++++++++---------- program/src/processor.rs | 24 ++++++ 4 files changed, 65 insertions(+), 44 deletions(-) diff --git a/interface/src/extension/permissioned_burn/mod.rs b/interface/src/extension/permissioned_burn/mod.rs index 8bc07eecf..f533d8212 100644 --- a/interface/src/extension/permissioned_burn/mod.rs +++ b/interface/src/extension/permissioned_burn/mod.rs @@ -33,5 +33,5 @@ impl Extension for PermissionedBurnConfig { } impl Extension for PermissionedBurnAccount { - const TYPE: ExtensionType = ExtensionType::PermissionedBurn; + const TYPE: ExtensionType = ExtensionType::PermissionedBurnAccount; } diff --git a/program/src/extension/permissioned_burn/mod.rs b/program/src/extension/permissioned_burn/mod.rs index 58e2a3946..1331e2bd8 100644 --- a/program/src/extension/permissioned_burn/mod.rs +++ b/program/src/extension/permissioned_burn/mod.rs @@ -1,6 +1,6 @@ -/// Instruction types for the pausable extension +/// Instruction types for the permissioned burn extension pub mod instruction; -/// Instruction processor for the pausable extension +/// Instruction processor for the permissioned burn extension pub mod processor; #[deprecated( diff --git a/program/src/extension/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs index f6867eab6..e258d8b49 100644 --- a/program/src/extension/permissioned_burn/processor.rs +++ b/program/src/extension/permissioned_burn/processor.rs @@ -8,9 +8,9 @@ use { check_program_account, error::TokenError, extension::{ - pausable::{ - instruction::{InitializeInstructionData, PausableInstruction}, - PausableConfig, + permissioned_burn::{ + instruction::{EnableInstructionData, PermissionedBurnInstruction}, + PermissionedBurnConfig, }, BaseStateWithExtensionsMut, PodStateWithExtensionsMut, }, @@ -19,7 +19,7 @@ use { }, }; -fn process_initialize( +fn process_enable( _program_id: &Pubkey, accounts: &[AccountInfo], authority: &Pubkey, @@ -29,40 +29,40 @@ fn process_initialize( 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)?; + let extension = mint.init_extension::(true)?; extension.authority = Some(*authority).try_into()?; Ok(()) } -/// Pause or resume minting / burning / transferring on the mint -fn process_toggle_pause( - program_id: &Pubkey, - accounts: &[AccountInfo], - pause: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - let mint_account_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); +// /// Enable or disable permissioned burn. +// fn process_toggle_permissioned_burn( +// program_id: &Pubkey, +// accounts: &[AccountInfo], +// enable: bool, +// ) -> ProgramResult { +// let account_info_iter = &mut accounts.iter(); +// let mint_account_info = next_account_info(account_info_iter)?; +// let authority_info = next_account_info(account_info_iter)?; +// let authority_info_data_len = authority_info.data_len(); - let mut mint_data = mint_account_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - let extension = mint.get_extension_mut::()?; - let maybe_authority: Option = extension.authority.into(); - let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; +// let mut mint_data = mint_account_info.data.borrow_mut(); +// let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; +// let extension = mint.get_extension_mut::()?; +// let maybe_authority: Option = extension.authority.into(); +// let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - Processor::validate_owner( - program_id, - &authority, - authority_info, - authority_info_data_len, - account_info_iter.as_slice(), - )?; +// Processor::validate_owner( +// program_id, +// &authority, +// authority_info, +// authority_info_data_len, +// account_info_iter.as_slice(), +// )?; - extension.paused = pause.into(); - Ok(()) -} +// extension.enabled = enable.into(); +// Ok(()) +// } pub(crate) fn process_instruction( program_id: &Pubkey, @@ -72,18 +72,15 @@ pub(crate) fn process_instruction( check_program_account(program_id)?; match decode_instruction_type(input)? { - PausableInstruction::Initialize => { - msg!("PausableInstruction::Initialize"); - let InitializeInstructionData { authority } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority) - } - PausableInstruction::Pause => { - msg!("PausableInstruction::Pause"); - process_toggle_pause(program_id, accounts, true /* pause */) + PermissionedBurnInstruction::Enable => { + msg!("PermissionedBurnInstruction::Enable"); + let EnableInstructionData { authority } = decode_instruction_data(input)?; + process_enable(program_id, accounts, authority) } - PausableInstruction::Resume => { - msg!("PausableInstruction::Resume"); - process_toggle_pause(program_id, accounts, false /* resume */) + PermissionedBurnInstruction::Disable => { + msg!("PermissionedBurnInstruction::Disable"); + Ok(()) + // process_toggle_permissioned_burn(program_id, accounts, false) } } } diff --git a/program/src/processor.rs b/program/src/processor.rs index 853da8486..9a8f635fb 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -50,6 +50,7 @@ use { mint_close_authority::MintCloseAuthority, non_transferable::{NonTransferable, NonTransferableAccount}, pausable::{PausableAccount, PausableConfig}, + permissioned_burn::{PermissionedBurnAccount, PermissionedBurnConfig}, permanent_delegate::{get_permanent_delegate, PermanentDelegate}, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, @@ -1109,6 +1110,29 @@ impl Processor { return Err(TokenError::MintPaused.into()); } } + if let Ok(ext) = mint.get_extension::() { + if ext.enabled.into() { + // Pull the required extra signer from the accounts + let approver_ai = next_account_info(account_info_iter)?; + + // Decode the configured approver from the mint extension + let required_approver: Option = Option::::from(ext.authority); + + // Enforce: approver must be present in config + let Some(req_key) = required_approver else { + return Err(ProgramError::InvalidAccountData); + }; + + if !approver_ai.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + + if *approver_ai.key != req_key { + return Err(ProgramError::InvalidAccountData); + } + } + } + let maybe_permanent_delegate = get_permanent_delegate(&mint); if let Ok(cpi_guard) = source_account.get_extension::() { From d918f27920a3d23c942c4ee3c96e01f629cc61d9 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 31 Oct 2025 23:26:56 +0100 Subject: [PATCH 04/32] cleanup --- .../permissioned_burn/instruction.rs | 48 ++++-------------- .../src/extension/permissioned_burn/mod.rs | 6 +-- .../extension/permissioned_burn/processor.rs | 50 +++---------------- program/src/processor.rs | 26 +++------- 4 files changed, 26 insertions(+), 104 deletions(-) diff --git a/interface/src/extension/permissioned_burn/instruction.rs b/interface/src/extension/permissioned_burn/instruction.rs index 6850c6fb6..c46143a20 100644 --- a/interface/src/extension/permissioned_burn/instruction.rs +++ b/interface/src/extension/permissioned_burn/instruction.rs @@ -22,31 +22,25 @@ pub enum PermissionedBurnInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writable]` The mint account for which to enable. + /// 0. `[writable]` The mint account to initialize. /// /// Data expected by this instruction: - /// `crate::extension::permissioned_burn::instruction::EnableInstructionData` - Enable, - /// Stop requiring burn to be signed by an additional authority. - /// - /// Accounts expected by this instruction: - /// - /// 0. `[writable]` The mint account for which to enable. - Disable, + /// `crate::extension::permissioned_burn::instruction::InitializeInstructionData` + Initialize, } -/// Data expected by `PermissionedBurnInstruction::Enable` +/// 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 EnableInstructionData { +pub struct InitializeInstructionData { /// The public key for the account that is required for token burning. pub authority: Pubkey, } -/// Create an `Enable` instruction -pub fn enable( +/// Create an `Initialize` instruction +pub fn initialize( token_program_id: &Pubkey, mint: &Pubkey, authority: &Pubkey, @@ -57,33 +51,9 @@ pub fn enable( token_program_id, accounts, TokenInstruction::PermissionedBurnExtension, - PermissionedBurnInstruction::Enable, - &EnableInstructionData { + PermissionedBurnInstruction::Initialize, + &InitializeInstructionData { authority: *authority, }, )) } - -/// Create a `Disable` instruction -pub fn disable( - token_program_id: &Pubkey, - mint: &Pubkey, - authority: &Pubkey, - signers: &[&Pubkey], -) -> Result { - check_program_account(token_program_id)?; - let mut accounts = vec![ - AccountMeta::new(*mint, false), - AccountMeta::new_readonly(*authority, signers.is_empty()), - ]; - for signer_pubkey in signers.iter() { - accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); - } - Ok(encode_instruction( - token_program_id, - accounts, - TokenInstruction::PermissionedBurnExtension, - PermissionedBurnInstruction::Disable, - &(), - )) -} diff --git a/interface/src/extension/permissioned_burn/mod.rs b/interface/src/extension/permissioned_burn/mod.rs index f533d8212..27c42902c 100644 --- a/interface/src/extension/permissioned_burn/mod.rs +++ b/interface/src/extension/permissioned_burn/mod.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use { crate::extension::{Extension, ExtensionType}, bytemuck::{Pod, Zeroable}, - spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}, + solana_pubkey::Pubkey }; /// Instruction types for the permissioned burn extension @@ -16,9 +16,7 @@ pub mod instruction; #[repr(C)] pub struct PermissionedBurnConfig { /// Authority that is required for burning - pub authority: OptionalNonZeroPubkey, - /// Whether permission from the authority is required to burn - pub enabled: PodBool, + pub authority: Pubkey, } /// Indicates that the tokens from this account belong to a permissioned burn mint diff --git a/program/src/extension/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs index e258d8b49..1e54d7b3f 100644 --- a/program/src/extension/permissioned_burn/processor.rs +++ b/program/src/extension/permissioned_burn/processor.rs @@ -1,15 +1,13 @@ use { - crate::processor::Processor, solana_account_info::{next_account_info, AccountInfo}, solana_msg::msg, solana_program_error::ProgramResult, solana_pubkey::Pubkey, spl_token_2022_interface::{ check_program_account, - error::TokenError, extension::{ permissioned_burn::{ - instruction::{EnableInstructionData, PermissionedBurnInstruction}, + instruction::{InitializeInstructionData, PermissionedBurnInstruction}, PermissionedBurnConfig, }, BaseStateWithExtensionsMut, PodStateWithExtensionsMut, @@ -19,7 +17,7 @@ use { }, }; -fn process_enable( +fn process_initialize( _program_id: &Pubkey, accounts: &[AccountInfo], authority: &Pubkey, @@ -30,40 +28,11 @@ fn process_enable( let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; let extension = mint.init_extension::(true)?; - extension.authority = Some(*authority).try_into()?; + extension.authority = *authority; Ok(()) } -// /// Enable or disable permissioned burn. -// fn process_toggle_permissioned_burn( -// program_id: &Pubkey, -// accounts: &[AccountInfo], -// enable: bool, -// ) -> ProgramResult { -// let account_info_iter = &mut accounts.iter(); -// let mint_account_info = next_account_info(account_info_iter)?; -// let authority_info = next_account_info(account_info_iter)?; -// let authority_info_data_len = authority_info.data_len(); - -// let mut mint_data = mint_account_info.data.borrow_mut(); -// let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; -// let extension = mint.get_extension_mut::()?; -// let maybe_authority: Option = extension.authority.into(); -// let authority = maybe_authority.ok_or(TokenError::AuthorityTypeNotSupported)?; - -// Processor::validate_owner( -// program_id, -// &authority, -// authority_info, -// authority_info_data_len, -// account_info_iter.as_slice(), -// )?; - -// extension.enabled = enable.into(); -// Ok(()) -// } - pub(crate) fn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], @@ -72,15 +41,10 @@ pub(crate) fn process_instruction( check_program_account(program_id)?; match decode_instruction_type(input)? { - PermissionedBurnInstruction::Enable => { - msg!("PermissionedBurnInstruction::Enable"); - let EnableInstructionData { authority } = decode_instruction_data(input)?; - process_enable(program_id, accounts, authority) - } - PermissionedBurnInstruction::Disable => { - msg!("PermissionedBurnInstruction::Disable"); - Ok(()) - // process_toggle_permissioned_burn(program_id, accounts, false) + PermissionedBurnInstruction::Initialize => { + msg!("PermissionedBurnInstruction::Initialize"); + let InitializeInstructionData { authority } = decode_instruction_data(input)?; + process_initialize(program_id, accounts, authority) } } } diff --git a/program/src/processor.rs b/program/src/processor.rs index 9a8f635fb..68a7786ef 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -50,7 +50,7 @@ use { mint_close_authority::MintCloseAuthority, non_transferable::{NonTransferable, NonTransferableAccount}, pausable::{PausableAccount, PausableConfig}, - permissioned_burn::{PermissionedBurnAccount, PermissionedBurnConfig}, + permissioned_burn::PermissionedBurnConfig, permanent_delegate::{get_permanent_delegate, PermanentDelegate}, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, @@ -1111,25 +1111,15 @@ impl Processor { } } if let Ok(ext) = mint.get_extension::() { - if ext.enabled.into() { - // Pull the required extra signer from the accounts - let approver_ai = next_account_info(account_info_iter)?; + // Pull the required extra signer from the accounts + let approver_ai = next_account_info(account_info_iter)?; - // Decode the configured approver from the mint extension - let required_approver: Option = Option::::from(ext.authority); - - // Enforce: approver must be present in config - let Some(req_key) = required_approver else { - return Err(ProgramError::InvalidAccountData); - }; - - if !approver_ai.is_signer { - return Err(ProgramError::MissingRequiredSignature); - } + if !approver_ai.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } - if *approver_ai.key != req_key { - return Err(ProgramError::InvalidAccountData); - } + if *approver_ai.key != ext.authority { + return Err(ProgramError::InvalidAccountData); } } From 6a2e5bd8bd41df8e7e86c25d3c8781c6140083b5 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 31 Oct 2025 23:30:06 +0100 Subject: [PATCH 05/32] typo --- program/src/extension/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program/src/extension/mod.rs b/program/src/extension/mod.rs index 95ba3fa07..f8474c024 100644 --- a/program/src/extension/mod.rs +++ b/program/src/extension/mod.rs @@ -26,7 +26,7 @@ pub mod mint_close_authority; pub mod non_transferable; /// Pausable extension pub mod pausable; -/// Pausable extension +/// Permissioned burn extension pub mod permissioned_burn; /// Permanent Delegate extension pub mod permanent_delegate; From 21b2e6525da791239ff3aa190eb621d05bc85502 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 31 Oct 2025 23:33:37 +0100 Subject: [PATCH 06/32] fmt --- interface/src/extension/mod.rs | 14 ++++++++------ interface/src/extension/permissioned_burn/mod.rs | 2 +- program/src/extension/mod.rs | 4 ++-- program/src/extension/permissioned_burn/mod.rs | 4 +++- program/src/processor.rs | 14 +++++++++----- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/interface/src/extension/mod.rs b/interface/src/extension/mod.rs index 529b73a3b..9652d71ad 100644 --- a/interface/src/extension/mod.rs +++ b/interface/src/extension/mod.rs @@ -22,8 +22,8 @@ use { mint_close_authority::MintCloseAuthority, non_transferable::{NonTransferable, NonTransferableAccount}, pausable::{PausableAccount, PausableConfig}, - permissioned_burn::{PermissionedBurnAccount, PermissionedBurnConfig}, permanent_delegate::PermanentDelegate, + permissioned_burn::{PermissionedBurnAccount, PermissionedBurnConfig}, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, transfer_hook::{TransferHook, TransferHookAccount}, @@ -75,10 +75,10 @@ pub mod mint_close_authority; pub mod non_transferable; /// Pausable extension pub mod pausable; -/// Permissioned burn extension -pub mod permissioned_burn; /// 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 @@ -1212,7 +1212,9 @@ impl ExtensionType { ExtensionType::Pausable => pod_get_packed_len::(), ExtensionType::PausableAccount => pod_get_packed_len::(), ExtensionType::PermissionedBurn => pod_get_packed_len::(), - ExtensionType::PermissionedBurnAccount => pod_get_packed_len::(), + ExtensionType::PermissionedBurnAccount => { + pod_get_packed_len::() + } #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1280,7 +1282,7 @@ impl ExtensionType { | ExtensionType::TokenGroupMember | ExtensionType::ScaledUiAmount | ExtensionType::Pausable => AccountType::Mint, - | ExtensionType::PermissionedBurn => AccountType::Mint, + ExtensionType::PermissionedBurn => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount @@ -1290,7 +1292,7 @@ impl ExtensionType { | ExtensionType::CpiGuard | ExtensionType::ConfidentialTransferFeeAmount | ExtensionType::PausableAccount => AccountType::Account, - | ExtensionType::PermissionedBurnAccount => AccountType::Account, + ExtensionType::PermissionedBurnAccount => AccountType::Account, #[cfg(test)] ExtensionType::VariableLenMintTest => AccountType::Mint, #[cfg(test)] diff --git a/interface/src/extension/permissioned_burn/mod.rs b/interface/src/extension/permissioned_burn/mod.rs index 27c42902c..37a701bfc 100644 --- a/interface/src/extension/permissioned_burn/mod.rs +++ b/interface/src/extension/permissioned_burn/mod.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use { crate::extension::{Extension, ExtensionType}, bytemuck::{Pod, Zeroable}, - solana_pubkey::Pubkey + solana_pubkey::Pubkey, }; /// Instruction types for the permissioned burn extension diff --git a/program/src/extension/mod.rs b/program/src/extension/mod.rs index f8474c024..aadb95847 100644 --- a/program/src/extension/mod.rs +++ b/program/src/extension/mod.rs @@ -26,10 +26,10 @@ pub mod mint_close_authority; pub mod non_transferable; /// Pausable extension pub mod pausable; -/// Permissioned burn extension -pub mod permissioned_burn; /// 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/mod.rs b/program/src/extension/permissioned_burn/mod.rs index 1331e2bd8..afa0f6a76 100644 --- a/program/src/extension/permissioned_burn/mod.rs +++ b/program/src/extension/permissioned_burn/mod.rs @@ -7,4 +7,6 @@ pub mod processor; 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::{PermissionedBurnAccount, PermissionedBurnConfig}; +pub use spl_token_2022_interface::extension::permissioned_burn::{ + PermissionedBurnAccount, PermissionedBurnConfig, +}; diff --git a/program/src/processor.rs b/program/src/processor.rs index 68a7786ef..393dbada1 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, permissioned_burn, + 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, @@ -50,8 +50,8 @@ use { mint_close_authority::MintCloseAuthority, non_transferable::{NonTransferable, NonTransferableAccount}, pausable::{PausableAccount, PausableConfig}, - permissioned_burn::PermissionedBurnConfig, permanent_delegate::{get_permanent_delegate, PermanentDelegate}, + permissioned_burn::PermissionedBurnConfig, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, transfer_hook::{TransferHook, TransferHookAccount}, @@ -1118,7 +1118,7 @@ impl Processor { return Err(ProgramError::MissingRequiredSignature); } - if *approver_ai.key != ext.authority { + if *approver_ai.key != ext.authority { return Err(ProgramError::InvalidAccountData); } } @@ -1958,7 +1958,11 @@ impl Processor { } PodTokenInstruction::PermissionedBurnExtension => { msg!("Instruction: PermissionedBurnExtension"); - permissioned_burn::processor::process_instruction(program_id, accounts, &input[1..]) + permissioned_burn::processor::process_instruction( + program_id, + accounts, + &input[1..], + ) } } } else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) { From e5ca5d411745b99573260769bfa255affaf7b7d0 Mon Sep 17 00:00:00 2001 From: Sergej Date: Sat, 1 Nov 2025 11:10:49 +0100 Subject: [PATCH 07/32] remove unnecessary PermissionedBurnAccount --- interface/src/extension/mod.rs | 8 +------- interface/src/extension/permissioned_burn/mod.rs | 11 ----------- program/src/extension/permissioned_burn/mod.rs | 4 +--- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/interface/src/extension/mod.rs b/interface/src/extension/mod.rs index 9652d71ad..ada61d194 100644 --- a/interface/src/extension/mod.rs +++ b/interface/src/extension/mod.rs @@ -23,7 +23,7 @@ use { non_transferable::{NonTransferable, NonTransferableAccount}, pausable::{PausableAccount, PausableConfig}, permanent_delegate::PermanentDelegate, - permissioned_burn::{PermissionedBurnAccount, PermissionedBurnConfig}, + permissioned_burn::PermissionedBurnConfig, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, transfer_hook::{TransferHook, TransferHookAccount}, @@ -1124,8 +1124,6 @@ pub enum ExtensionType { PausableAccount, /// Tokens burning requires approval from authorirty. PermissionedBurn, - /// Indicates that the account belongs to a mint requiring permissioned burn. - PermissionedBurnAccount, /// Test variable-length mint extension #[cfg(test)] @@ -1212,9 +1210,6 @@ impl ExtensionType { ExtensionType::Pausable => pod_get_packed_len::(), ExtensionType::PausableAccount => pod_get_packed_len::(), ExtensionType::PermissionedBurn => pod_get_packed_len::(), - ExtensionType::PermissionedBurnAccount => { - pod_get_packed_len::() - } #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1292,7 +1287,6 @@ impl ExtensionType { | ExtensionType::CpiGuard | ExtensionType::ConfidentialTransferFeeAmount | ExtensionType::PausableAccount => AccountType::Account, - ExtensionType::PermissionedBurnAccount => AccountType::Account, #[cfg(test)] ExtensionType::VariableLenMintTest => AccountType::Mint, #[cfg(test)] diff --git a/interface/src/extension/permissioned_burn/mod.rs b/interface/src/extension/permissioned_burn/mod.rs index 37a701bfc..7f7f41fe8 100644 --- a/interface/src/extension/permissioned_burn/mod.rs +++ b/interface/src/extension/permissioned_burn/mod.rs @@ -19,17 +19,6 @@ pub struct PermissionedBurnConfig { pub authority: Pubkey, } -/// Indicates that the tokens from this account belong to a permissioned burn mint -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] -#[repr(transparent)] -pub struct PermissionedBurnAccount; - impl Extension for PermissionedBurnConfig { const TYPE: ExtensionType = ExtensionType::PermissionedBurn; } - -impl Extension for PermissionedBurnAccount { - const TYPE: ExtensionType = ExtensionType::PermissionedBurnAccount; -} diff --git a/program/src/extension/permissioned_burn/mod.rs b/program/src/extension/permissioned_burn/mod.rs index afa0f6a76..cf8bbbfcd 100644 --- a/program/src/extension/permissioned_burn/mod.rs +++ b/program/src/extension/permissioned_burn/mod.rs @@ -7,6 +7,4 @@ pub mod processor; 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::{ - PermissionedBurnAccount, PermissionedBurnConfig, -}; +pub use spl_token_2022_interface::extension::permissioned_burn::PermissionedBurnConfig; From d533794f89c0c32d0735f00602b93c9a3f781386 Mon Sep 17 00:00:00 2001 From: Sergej Sakac <73715684+Szegoo@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:43:03 +0100 Subject: [PATCH 08/32] Update interface/src/extension/mod.rs Co-authored-by: Jon C --- interface/src/extension/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/extension/mod.rs b/interface/src/extension/mod.rs index ada61d194..c6a84de93 100644 --- a/interface/src/extension/mod.rs +++ b/interface/src/extension/mod.rs @@ -1276,8 +1276,8 @@ impl ExtensionType { | ExtensionType::ConfidentialMintBurn | ExtensionType::TokenGroupMember | ExtensionType::ScaledUiAmount - | ExtensionType::Pausable => AccountType::Mint, - ExtensionType::PermissionedBurn => AccountType::Mint, + | ExtensionType::Pausable + | ExtensionType::PermissionedBurn => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount From c22a5feb17f76d5275b773ff78bb2002a5966e31 Mon Sep 17 00:00:00 2001 From: Sergej Date: Tue, 4 Nov 2025 19:47:36 +0100 Subject: [PATCH 09/32] add missing unpack & change index to 46 --- interface/src/extension/mod.rs | 4 ++-- interface/src/instruction.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/src/extension/mod.rs b/interface/src/extension/mod.rs index c6a84de93..4065e1446 100644 --- a/interface/src/extension/mod.rs +++ b/interface/src/extension/mod.rs @@ -1276,8 +1276,8 @@ impl ExtensionType { | ExtensionType::ConfidentialMintBurn | ExtensionType::TokenGroupMember | ExtensionType::ScaledUiAmount - | ExtensionType::Pausable - | ExtensionType::PermissionedBurn => AccountType::Mint, + | ExtensionType::Pausable + | ExtensionType::PermissionedBurn => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index d80cee8d4..0f64ad2b4 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -875,6 +875,7 @@ impl<'a> TokenInstruction<'a> { 42 => Self::ConfidentialMintBurnExtension, 43 => Self::ScaledUiAmountExtension, 44 => Self::PausableExtension, + 46 => Self::PermissionedBurnExtension, _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -1056,7 +1057,7 @@ impl<'a> TokenInstruction<'a> { buf.push(44); } &Self::PermissionedBurnExtension => { - buf.push(45); + buf.push(46); } }; buf From f9133d38a0e848b1e574a664425fe63d3ffd08ce Mon Sep 17 00:00:00 2001 From: Sergej Date: Wed, 5 Nov 2025 16:36:23 +0100 Subject: [PATCH 10/32] implement authority type --- interface/src/extension/permissioned_burn/mod.rs | 4 ++-- interface/src/instruction.rs | 4 ++++ .../src/extension/permissioned_burn/processor.rs | 2 +- program/src/processor.rs | 16 +++++++++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/interface/src/extension/permissioned_burn/mod.rs b/interface/src/extension/permissioned_burn/mod.rs index 7f7f41fe8..00e42831e 100644 --- a/interface/src/extension/permissioned_burn/mod.rs +++ b/interface/src/extension/permissioned_burn/mod.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use { crate::extension::{Extension, ExtensionType}, bytemuck::{Pod, Zeroable}, - solana_pubkey::Pubkey, + spl_pod::optional_keys::OptionalNonZeroPubkey }; /// Instruction types for the permissioned burn extension @@ -16,7 +16,7 @@ pub mod instruction; #[repr(C)] pub struct PermissionedBurnConfig { /// Authority that is required for burning - pub authority: Pubkey, + pub authority: OptionalNonZeroPubkey, } impl Extension for PermissionedBurnConfig { diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index 0f64ad2b4..fcb938ba0 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -1160,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 { @@ -1182,6 +1184,7 @@ impl AuthorityType { AuthorityType::GroupMemberPointer => 14, AuthorityType::ScaledUiAmount => 15, AuthorityType::Pause => 16, + AuthorityType::PermissionedBurn => 17, } } @@ -1205,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/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs index 1e54d7b3f..50f3bb838 100644 --- a/program/src/extension/permissioned_burn/processor.rs +++ b/program/src/extension/permissioned_burn/processor.rs @@ -28,7 +28,7 @@ fn process_initialize( let mut mint = PodStateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; let extension = mint.init_extension::(true)?; - extension.authority = *authority; + extension.authority = Some(*authority).try_into()?; Ok(()) } diff --git a/program/src/processor.rs b/program/src/processor.rs index 393dbada1..feec9dcd4 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -967,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()); } @@ -1118,7 +1131,8 @@ impl Processor { return Err(ProgramError::MissingRequiredSignature); } - if *approver_ai.key != ext.authority { + let maybe_burn_authority: Option = ext.authority.into(); + if Some(*approver_ai.key) != maybe_burn_authority { return Err(ProgramError::InvalidAccountData); } } From 52931dac51e6eb5e001a8ecb3865b8d05f735b32 Mon Sep 17 00:00:00 2001 From: Sergej Date: Thu, 6 Nov 2025 13:39:28 +0100 Subject: [PATCH 11/32] new PermissionedBurn instruction --- .../src/extension/permissioned_burn/mod.rs | 2 +- program/src/pod_instruction.rs | 2 + program/src/processor.rs | 74 +++++++++++++++---- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/interface/src/extension/permissioned_burn/mod.rs b/interface/src/extension/permissioned_burn/mod.rs index 00e42831e..636e38450 100644 --- a/interface/src/extension/permissioned_burn/mod.rs +++ b/interface/src/extension/permissioned_burn/mod.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use { crate::extension::{Extension, ExtensionType}, bytemuck::{Pod, Zeroable}, - spl_pod::optional_keys::OptionalNonZeroPubkey + spl_pod::optional_keys::OptionalNonZeroPubkey, }; /// Instruction types for the permissioned burn extension diff --git a/program/src/pod_instruction.rs b/program/src/pod_instruction.rs index 9dcaaa5cb..d79149f12 100644 --- a/program/src/pod_instruction.rs +++ b/program/src/pod_instruction.rs @@ -115,7 +115,9 @@ pub(crate) enum PodTokenInstruction { ConfidentialMintBurnExtension, ScaledUiAmountExtension, PausableExtension, + // 45 PermissionedBurnExtension, + PermissionedBurn, } fn unpack_pubkey_option(input: &[u8]) -> Result, ProgramError> { diff --git a/program/src/processor.rs b/program/src/processor.rs index feec9dcd4..6888a25ca 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1086,6 +1086,57 @@ impl Processor { accounts: &[AccountInfo], amount: u64, instruction_variant: InstructionVariant, + ) -> ProgramResult { + Self::do_process_burn(program_id, accounts, amount, instruction_variant) + } + + /// Processes a [`PermissionedBurn`](enum.TokenInstruction.html) instruction. + pub(crate) fn process_permissioned_burn( + program_id: &Pubkey, + accounts: &[AccountInfo], + amount: u64, + instruction_variant: InstructionVariant, + ) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let source_account_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let authority_info_data_len = authority_info.data_len(); + + let mut mint_data = mint_info.data.borrow_mut(); + let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; + + 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 remaining_after = account_info_iter.as_slice(); + let mut forward = vec![ + source_account_info.clone(), + mint_info.clone(), + authority_info.clone(), + ]; + forward.extend_from_slice(remaining_after); + + Self::do_process_burn(program_id, &forward, amount, instruction_variant) + } + + fn do_process_burn( + program_id: &Pubkey, + accounts: &[AccountInfo], + amount: u64, + instruction_variant: InstructionVariant, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -1123,19 +1174,6 @@ 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); @@ -1775,6 +1813,16 @@ impl Processor { InstructionVariant::Unchecked, ) } + PodTokenInstruction::PermissionedBurn => { + msg!("Instruction: PermissionedBurn"); + let data = decode_instruction_data::(input)?; + Self::process_burn( + program_id, + accounts, + data.amount.into(), + InstructionVariant::Unchecked, + ) + } PodTokenInstruction::CloseAccount => { msg!("Instruction: CloseAccount"); Self::process_close_account(program_id, accounts) From 8a04278fabfc070aab9f42f86b667e2decc3e6e3 Mon Sep 17 00:00:00 2001 From: Sergej Date: Sat, 8 Nov 2025 10:57:26 +0100 Subject: [PATCH 12/32] small fix & add to rust-legacy --- clients/rust-legacy/src/token.rs | 13 ++- .../rust-legacy/tests/permissioned_burn.rs | 100 ++++++++++++++++++ program/src/processor.rs | 3 +- 3 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 clients/rust-legacy/tests/permissioned_burn.rs diff --git a/clients/rust-legacy/src/token.rs b/clients/rust-legacy/src/token.rs index e3a845cad..8184a57aa 100644 --- a/clients/rust-legacy/src/token.rs +++ b/clients/rust-legacy/src/token.rs @@ -44,9 +44,9 @@ use { self, ConfidentialTransferFeeAmount, ConfidentialTransferFeeConfig, }, cpi_guard, default_account_state, group_member_pointer, group_pointer, - interest_bearing_mint, memo_transfer, metadata_pointer, pausable, scaled_ui_amount, - transfer_fee, transfer_hook, BaseStateWithExtensions, Extension, ExtensionType, - StateWithExtensionsOwned, + interest_bearing_mint, memo_transfer, metadata_pointer, pausable, permissioned_burn, + scaled_ui_amount, transfer_fee, transfer_hook, BaseStateWithExtensions, Extension, + ExtensionType, StateWithExtensionsOwned, }, instruction, solana_zk_sdk::{ @@ -201,6 +201,9 @@ pub enum ExtensionInitializationParams { PausableConfig { authority: Pubkey, }, + PermissionedBurnConfig { + authority: Pubkey, + }, ConfidentialMintBurn { supply_elgamal_pubkey: PodElGamalPubkey, decryptable_supply: PodAeCiphertext, @@ -226,6 +229,7 @@ impl ExtensionInitializationParams { Self::GroupMemberPointer { .. } => ExtensionType::GroupMemberPointer, Self::ScaledUiAmountConfig { .. } => ExtensionType::ScaledUiAmount, Self::PausableConfig { .. } => ExtensionType::Pausable, + Self::PermissionedBurnConfig { .. } => ExtensionType::PermissionedBurn, Self::ConfidentialMintBurn { .. } => ExtensionType::ConfidentialMintBurn, } } @@ -348,6 +352,9 @@ impl ExtensionInitializationParams { Self::PausableConfig { authority } => { pausable::instruction::initialize(token_program_id, mint, &authority) } + Self::PermissionedBurnConfig { authority } => { + permissioned_burn::instruction::initialize(token_program_id, mint, &authority) + } Self::ConfidentialMintBurn { supply_elgamal_pubkey, decryptable_supply, diff --git a/clients/rust-legacy/tests/permissioned_burn.rs b/clients/rust-legacy/tests/permissioned_burn.rs new file mode 100644 index 000000000..20c71837b --- /dev/null +++ b/clients/rust-legacy/tests/permissioned_burn.rs @@ -0,0 +1,100 @@ +mod program_test; +use { + program_test::{TestContext, TokenContext}, + solana_program_error::ProgramError, + solana_program_test::tokio, + solana_sdk::{ + instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, + transaction::TransactionError, transport::TransportError, + }, + spl_token_2022_interface::extension::BaseStateWithExtensions, + spl_token_2022_interface::{ + error::TokenError, extension::permissioned_burn::PermissionedBurnConfig, + }, + spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, +}; + +#[tokio::test] +async fn success_initialize() { + let authority = Pubkey::new_unique(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ + ExtensionInitializationParams::PermissionedBurnConfig { authority }, + ]) + .await + .unwrap(); + let TokenContext { + token, + mint_authority, + alice, + .. + } = context.token_context.unwrap(); + + let state = token.get_mint_info().await.unwrap(); + let extension = state.get_extension::().unwrap(); + assert_eq!(Option::::from(extension.authority), Some(authority)); + + // mint a token + let amount = 10; + token + .mint_to( + &alice.pubkey(), + &mint_authority.pubkey(), + amount, + &[&mint_authority], + ) + .await + .unwrap(); + + // regular burn fails + let error = token + .burn(&alice.pubkey(), &alice.pubkey(), 1, &[&alice]) + .await + .unwrap_err(); + // assert_eq!( + // error, + // TokenClientError::Client(Box::new(TransportError::TransactionError( + // TransactionError::InstructionError( + // 0, + // InstructionError::Custom(ProgramError::MissingRequiredSignature as u32) + // ) + // ))) + // ); + + // // checked is ok + // token + // .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) + // .await + // .unwrap(); + + // // burn too much is not ok + // let error = token + // .burn(&alice_account, &alice.pubkey(), amount, &[&alice]) + // .await + // .unwrap_err(); + // assert_eq!( + // error, + // TokenClientError::Client(Box::new(TransportError::TransactionError( + // TransactionError::InstructionError( + // 0, + // InstructionError::Custom(TokenError::InsufficientFunds as u32) + // ) + // ))) + // ); + + // // wrong signer + // let error = token + // .burn(&alice_account, &bob.pubkey(), 1, &[&bob]) + // .await + // .unwrap_err(); + // assert_eq!( + // error, + // TokenClientError::Client(Box::new(TransportError::TransactionError( + // TransactionError::InstructionError( + // 0, + // InstructionError::Custom(TokenError::OwnerMismatch as u32) + // ) + // ))) + // ); +} diff --git a/program/src/processor.rs b/program/src/processor.rs index 6888a25ca..4b23627a8 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1102,7 +1102,6 @@ impl Processor { let source_account_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; let authority_info = next_account_info(account_info_iter)?; - let authority_info_data_len = authority_info.data_len(); let mut mint_data = mint_info.data.borrow_mut(); let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; @@ -1816,7 +1815,7 @@ impl Processor { PodTokenInstruction::PermissionedBurn => { msg!("Instruction: PermissionedBurn"); let data = decode_instruction_data::(input)?; - Self::process_burn( + Self::process_permissioned_burn( program_id, accounts, data.amount.into(), From 05ee8e09dfecb79672274286450339789e64231c Mon Sep 17 00:00:00 2001 From: Sergej Date: Sat, 8 Nov 2025 15:13:51 +0100 Subject: [PATCH 13/32] remove --- .../rust-legacy/tests/permissioned_burn.rs | 100 ------------------ 1 file changed, 100 deletions(-) delete mode 100644 clients/rust-legacy/tests/permissioned_burn.rs diff --git a/clients/rust-legacy/tests/permissioned_burn.rs b/clients/rust-legacy/tests/permissioned_burn.rs deleted file mode 100644 index 20c71837b..000000000 --- a/clients/rust-legacy/tests/permissioned_burn.rs +++ /dev/null @@ -1,100 +0,0 @@ -mod program_test; -use { - program_test::{TestContext, TokenContext}, - solana_program_error::ProgramError, - solana_program_test::tokio, - solana_sdk::{ - instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, - transaction::TransactionError, transport::TransportError, - }, - spl_token_2022_interface::extension::BaseStateWithExtensions, - spl_token_2022_interface::{ - error::TokenError, extension::permissioned_burn::PermissionedBurnConfig, - }, - spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, -}; - -#[tokio::test] -async fn success_initialize() { - let authority = Pubkey::new_unique(); - let mut context = TestContext::new().await; - context - .init_token_with_mint(vec![ - ExtensionInitializationParams::PermissionedBurnConfig { authority }, - ]) - .await - .unwrap(); - let TokenContext { - token, - mint_authority, - alice, - .. - } = context.token_context.unwrap(); - - let state = token.get_mint_info().await.unwrap(); - let extension = state.get_extension::().unwrap(); - assert_eq!(Option::::from(extension.authority), Some(authority)); - - // mint a token - let amount = 10; - token - .mint_to( - &alice.pubkey(), - &mint_authority.pubkey(), - amount, - &[&mint_authority], - ) - .await - .unwrap(); - - // regular burn fails - let error = token - .burn(&alice.pubkey(), &alice.pubkey(), 1, &[&alice]) - .await - .unwrap_err(); - // assert_eq!( - // error, - // TokenClientError::Client(Box::new(TransportError::TransactionError( - // TransactionError::InstructionError( - // 0, - // InstructionError::Custom(ProgramError::MissingRequiredSignature as u32) - // ) - // ))) - // ); - - // // checked is ok - // token - // .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) - // .await - // .unwrap(); - - // // burn too much is not ok - // let error = token - // .burn(&alice_account, &alice.pubkey(), amount, &[&alice]) - // .await - // .unwrap_err(); - // assert_eq!( - // error, - // TokenClientError::Client(Box::new(TransportError::TransactionError( - // TransactionError::InstructionError( - // 0, - // InstructionError::Custom(TokenError::InsufficientFunds as u32) - // ) - // ))) - // ); - - // // wrong signer - // let error = token - // .burn(&alice_account, &bob.pubkey(), 1, &[&bob]) - // .await - // .unwrap_err(); - // assert_eq!( - // error, - // TokenClientError::Client(Box::new(TransportError::TransactionError( - // TransactionError::InstructionError( - // 0, - // InstructionError::Custom(TokenError::OwnerMismatch as u32) - // ) - // ))) - // ); -} From 88b3d0bae83d49aef9b91c2821c0769958652c3f Mon Sep 17 00:00:00 2001 From: Sergej Date: Wed, 26 Nov 2025 22:37:08 +0100 Subject: [PATCH 14/32] move instruction under PermissionedBurnExtension --- .../permissioned_burn/instruction.rs | 131 ++++++++++++++++++ .../extension/permissioned_burn/processor.rs | 26 ++++ program/src/pod_instruction.rs | 3 +- program/src/processor.rs | 12 +- 4 files changed, 159 insertions(+), 13 deletions(-) diff --git a/interface/src/extension/permissioned_burn/instruction.rs b/interface/src/extension/permissioned_burn/instruction.rs index c46143a20..8b120be10 100644 --- a/interface/src/extension/permissioned_burn/instruction.rs +++ b/interface/src/extension/permissioned_burn/instruction.rs @@ -10,6 +10,7 @@ use { solana_instruction::{AccountMeta, Instruction}, solana_program_error::ProgramError, solana_pubkey::Pubkey, + spl_pod::primitives::PodU64, }; /// Permissioned Burn extension instructions @@ -27,6 +28,34 @@ pub enum PermissionedBurnInstruction { /// Data expected by this instruction: /// `crate::extension::permissioned_burn::instruction::InitializeInstructionData` Initialize, + /// Burn tokens when the mint has the permissioned burn extension enabled. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The source account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[signer]` The source account's owner/delegate. + /// 3. `[signer]` The permissioned burn authority configured on the mint. + /// + /// * Multisignature authority + /// 0. `[writable]` The source account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[]` The source account's multisignature owner/delegate. + /// 3. `[signer]` The permissioned burn authority configured on the mint. + /// 4. `..4+M` `[signer]` M signer accounts for the multisig. + /// + /// Data expected by this instruction: + /// `crate::extension::permissioned_burn::instruction::BurnInstructionData` + Burn, + /// Burn tokens with expected decimals when the mint has the permissioned + /// burn extension enabled. + /// + /// Accounts expected by this instruction match `Burn`. + /// + /// Data expected by this instruction: + /// `crate::extension::permissioned_burn::instruction::BurnCheckedInstructionData` + BurnChecked, } /// Data expected by `PermissionedBurnInstruction::Initialize` @@ -39,6 +68,28 @@ pub struct InitializeInstructionData { pub authority: Pubkey, } +/// Data expected by `PermissionedBurnInstruction::Burn` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct BurnInstructionData { + /// The amount of tokens to burn. + pub amount: PodU64, +} + +/// Data expected by `PermissionedBurnInstruction::BurnChecked` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct BurnCheckedInstructionData { + /// The amount of tokens to burn. + pub amount: PodU64, + /// Expected number of base 10 digits to the right of the decimal place. + pub decimals: u8, +} + /// Create an `Initialize` instruction pub fn initialize( token_program_id: &Pubkey, @@ -57,3 +108,83 @@ pub fn initialize( }, )) } + +/// Create a `Burn` instruction using the permissioned burn extension. +pub fn burn( + token_program_id: &Pubkey, + account: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + permissioned_burn_authority: &Pubkey, + signer_pubkeys: &[&Pubkey], + amount: u64, +) -> Result { + check_program_account(token_program_id)?; + let data = BurnInstructionData { + amount: amount.into(), + }; + + let mut accounts = Vec::with_capacity(4 + signer_pubkeys.len()); + accounts.push(AccountMeta::new(*account, false)); + accounts.push(AccountMeta::new(*mint, false)); + accounts.push(AccountMeta::new_readonly( + *authority, + signer_pubkeys.is_empty(), + )); + accounts.push(AccountMeta::new_readonly( + *permissioned_burn_authority, + true, + )); + for signer_pubkey in signer_pubkeys.iter() { + accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); + } + + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::PermissionedBurnExtension, + PermissionedBurnInstruction::Burn, + &data, + )) +} + +/// Create a `BurnChecked` instruction using the permissioned burn extension. +pub fn burn_checked( + token_program_id: &Pubkey, + account: &Pubkey, + mint: &Pubkey, + authority: &Pubkey, + permissioned_burn_authority: &Pubkey, + signer_pubkeys: &[&Pubkey], + amount: u64, + decimals: u8, +) -> Result { + check_program_account(token_program_id)?; + let data = BurnCheckedInstructionData { + amount: amount.into(), + decimals, + }; + + let mut accounts = Vec::with_capacity(4 + signer_pubkeys.len()); + accounts.push(AccountMeta::new(*account, false)); + accounts.push(AccountMeta::new(*mint, false)); + accounts.push(AccountMeta::new_readonly( + *authority, + signer_pubkeys.is_empty(), + )); + accounts.push(AccountMeta::new_readonly( + *permissioned_burn_authority, + true, + )); + for signer_pubkey in signer_pubkeys.iter() { + accounts.push(AccountMeta::new_readonly(**signer_pubkey, true)); + } + + Ok(encode_instruction( + token_program_id, + accounts, + TokenInstruction::PermissionedBurnExtension, + PermissionedBurnInstruction::BurnChecked, + &data, + )) +} diff --git a/program/src/extension/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs index 50f3bb838..e70c089fb 100644 --- a/program/src/extension/permissioned_burn/processor.rs +++ b/program/src/extension/permissioned_burn/processor.rs @@ -1,4 +1,8 @@ use { + crate::{ + pod_instruction::{AmountCheckedData, AmountData}, + processor::{InstructionVariant, Processor}, + }, solana_account_info::{next_account_info, AccountInfo}, solana_msg::msg, solana_program_error::ProgramResult, @@ -46,5 +50,27 @@ pub(crate) fn process_instruction( let InitializeInstructionData { authority } = decode_instruction_data(input)?; process_initialize(program_id, accounts, authority) } + PermissionedBurnInstruction::Burn => { + msg!("PermissionedBurnInstruction::Burn"); + let data = decode_instruction_data::(input)?; + Processor::process_permissioned_burn( + program_id, + accounts, + data.amount.into(), + InstructionVariant::Unchecked, + ) + } + PermissionedBurnInstruction::BurnChecked => { + msg!("PermissionedBurnInstruction::BurnChecked"); + let data = decode_instruction_data::(input)?; + Processor::process_permissioned_burn( + program_id, + accounts, + data.amount.into(), + InstructionVariant::Checked { + decimals: data.decimals, + }, + ) + } } } diff --git a/program/src/pod_instruction.rs b/program/src/pod_instruction.rs index d79149f12..ce4b2b215 100644 --- a/program/src/pod_instruction.rs +++ b/program/src/pod_instruction.rs @@ -116,8 +116,7 @@ pub(crate) enum PodTokenInstruction { ScaledUiAmountExtension, PausableExtension, // 45 - PermissionedBurnExtension, - PermissionedBurn, + PermissionedBurnExtension = 46, } fn unpack_pubkey_option(input: &[u8]) -> Result, ProgramError> { diff --git a/program/src/processor.rs b/program/src/processor.rs index 4b23627a8..86c6d893e 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1090,7 +1090,7 @@ impl Processor { Self::do_process_burn(program_id, accounts, amount, instruction_variant) } - /// Processes a [`PermissionedBurn`](enum.TokenInstruction.html) instruction. + /// Processes a permissioned burn extension instruction. pub(crate) fn process_permissioned_burn( program_id: &Pubkey, accounts: &[AccountInfo], @@ -1812,16 +1812,6 @@ impl Processor { InstructionVariant::Unchecked, ) } - PodTokenInstruction::PermissionedBurn => { - msg!("Instruction: PermissionedBurn"); - let data = decode_instruction_data::(input)?; - Self::process_permissioned_burn( - program_id, - accounts, - data.amount.into(), - InstructionVariant::Unchecked, - ) - } PodTokenInstruction::CloseAccount => { msg!("Instruction: CloseAccount"); Self::process_close_account(program_id, accounts) From 32baa702f163bd45ce97cdcea96811b54eebc1f5 Mon Sep 17 00:00:00 2001 From: Sergej Date: Wed, 26 Nov 2025 22:51:27 +0100 Subject: [PATCH 15/32] refactor --- .../extension/permissioned_burn/processor.rs | 12 +-- program/src/processor.rs | 84 +++++++++++-------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/program/src/extension/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs index e70c089fb..fd1130162 100644 --- a/program/src/extension/permissioned_burn/processor.rs +++ b/program/src/extension/permissioned_burn/processor.rs @@ -1,7 +1,7 @@ use { crate::{ pod_instruction::{AmountCheckedData, AmountData}, - processor::{InstructionVariant, Processor}, + processor::{BurnInstructionVariant, InstructionVariant, Processor}, }, solana_account_info::{next_account_info, AccountInfo}, solana_msg::msg, @@ -53,23 +53,23 @@ pub(crate) fn process_instruction( PermissionedBurnInstruction::Burn => { msg!("PermissionedBurnInstruction::Burn"); let data = decode_instruction_data::(input)?; - Processor::process_permissioned_burn( + Processor::process_burn( program_id, accounts, data.amount.into(), - InstructionVariant::Unchecked, + BurnInstructionVariant::Permissioned(InstructionVariant::Unchecked), ) } PermissionedBurnInstruction::BurnChecked => { msg!("PermissionedBurnInstruction::BurnChecked"); let data = decode_instruction_data::(input)?; - Processor::process_permissioned_burn( + Processor::process_burn( program_id, accounts, data.amount.into(), - InstructionVariant::Checked { + BurnInstructionVariant::Permissioned(InstructionVariant::Checked { decimals: data.decimals, - }, + }), ) } } diff --git a/program/src/processor.rs b/program/src/processor.rs index 86c6d893e..7d192acf3 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -82,6 +82,15 @@ pub(crate) enum InstructionVariant { Checked { decimals: u8 }, } +/// Burn instruction variant. Standard variants must not be used with the +/// permissioned burn extension. +/// +/// Permissioned variants require the extra authority to sign. +pub(crate) enum BurnInstructionVariant { + Standard(InstructionVariant), + Permissioned(InstructionVariant), +} + /// Program state handler. pub struct Processor {} impl Processor { @@ -1085,17 +1094,7 @@ impl Processor { program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, - instruction_variant: InstructionVariant, - ) -> ProgramResult { - Self::do_process_burn(program_id, accounts, amount, instruction_variant) - } - - /// Processes a permissioned burn extension instruction. - pub(crate) fn process_permissioned_burn( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - instruction_variant: InstructionVariant, + instruction_variant: BurnInstructionVariant, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -1104,40 +1103,51 @@ impl Processor { let authority_info = next_account_info(account_info_iter)?; let mut mint_data = mint_info.data.borrow_mut(); - let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; + let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; - if let Ok(ext) = mint.get_extension::() { - // Pull the required extra signer from the accounts - let approver_ai = next_account_info(account_info_iter)?; + let permissioned_ext = mint.get_extension::(); + let mut forward = vec![ + source_account_info.clone(), + mint_info.clone(), + authority_info.clone(), + ]; - if !approver_ai.is_signer { - return Err(ProgramError::MissingRequiredSignature); + match instruction_variant { + BurnInstructionVariant::Standard(_) => { + // Standard burns cannot be used when the permissioned burn + // extension is present. + if permissioned_ext.is_ok() { + return Err(TokenError::InvalidInstruction.into()); + } } + BurnInstructionVariant::Permissioned(_) => { + let ext = permissioned_ext.map_err(|_| TokenError::InvalidInstruction)?; - let maybe_burn_authority: Option = ext.authority.into(); - if Some(*approver_ai.key) != maybe_burn_authority { - return Err(ProgramError::InvalidAccountData); + // 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); + } + + forward.push(approver_ai.clone()); } } let remaining_after = account_info_iter.as_slice(); - let mut forward = vec![ - source_account_info.clone(), - mint_info.clone(), - authority_info.clone(), - ]; forward.extend_from_slice(remaining_after); - Self::do_process_burn(program_id, &forward, amount, instruction_variant) - } + let instruction_variant = match instruction_variant { + BurnInstructionVariant::Standard(v) | BurnInstructionVariant::Permissioned(v) => v, + }; - fn do_process_burn( - program_id: &Pubkey, - accounts: &[AccountInfo], - amount: u64, - instruction_variant: InstructionVariant, - ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); + let burn_accounts = forward; + let account_info_iter = &mut burn_accounts.iter(); let source_account_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; @@ -1809,7 +1819,7 @@ impl Processor { program_id, accounts, data.amount.into(), - InstructionVariant::Unchecked, + BurnInstructionVariant::Standard(InstructionVariant::Unchecked), ) } PodTokenInstruction::CloseAccount => { @@ -1867,9 +1877,9 @@ impl Processor { program_id, accounts, data.amount.into(), - InstructionVariant::Checked { + BurnInstructionVariant::Standard(InstructionVariant::Checked { decimals: data.decimals, - }, + }), ) } PodTokenInstruction::SyncNative => { From c062644bae8770db4f7cf5ba8d977225cff91dbb Mon Sep 17 00:00:00 2001 From: Sergej Date: Thu, 27 Nov 2025 10:16:00 +0100 Subject: [PATCH 16/32] rust-legacy test --- .../rust-legacy/tests/permissioned_burn.rs | 135 ++++++++++++++++++ program/src/processor.rs | 2 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 clients/rust-legacy/tests/permissioned_burn.rs diff --git a/clients/rust-legacy/tests/permissioned_burn.rs b/clients/rust-legacy/tests/permissioned_burn.rs new file mode 100644 index 000000000..539d06614 --- /dev/null +++ b/clients/rust-legacy/tests/permissioned_burn.rs @@ -0,0 +1,135 @@ +mod program_test; +use { + program_test::{TestContext, TokenContext}, + solana_program_test::tokio, + solana_sdk::{ + instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair, + transaction::TransactionError, transport::TransportError, + }, + spl_token_2022_interface::{ + error::TokenError, + extension::{ + permissioned_burn::{ + instruction as permissioned_burn_instruction, PermissionedBurnConfig, + }, + BaseStateWithExtensions, + }, + }, + spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, +}; + +fn client_error(token_error: TokenError) -> TokenClientError { + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::Custom(token_error as u32)), + ))) +} + +#[tokio::test] +async fn success_initialize() { + let mut context = TestContext::new().await; + let authority = Keypair::new(); + context + .init_token_with_mint(vec![ + ExtensionInitializationParams::PermissionedBurnConfig { + authority: authority.pubkey(), + }, + ]) + .await + .unwrap(); + + let TokenContext { token, .. } = context.token_context.unwrap(); + let state = token.get_mint_info().await.unwrap(); + let extension = state.get_extension::().unwrap(); + + assert_eq!( + Option::::from(extension.authority), + Some(authority.pubkey()) + ); +} + +#[tokio::test] +async fn permissioned_burn_enforced() { + let mut context = TestContext::new().await; + let authority = Keypair::new(); + context + .init_token_with_mint(vec![ + ExtensionInitializationParams::PermissionedBurnConfig { + authority: authority.pubkey(), + }, + ]) + .await + .unwrap(); + + let TokenContext { + token, + mint_authority, + decimals, + .. + } = context.token_context.unwrap(); + + let account_owner = Keypair::new(); + token + .create_auxiliary_token_account(&account_owner, &account_owner.pubkey()) + .await + .unwrap(); + let account = account_owner.pubkey(); + + // Mint some supply + token + .mint_to(&account, &mint_authority.pubkey(), 2, &[&mint_authority]) + .await + .unwrap(); + + // Standard burn should be rejected when the permissioned extension is set. + let error = token + .burn(&account, &account_owner.pubkey(), 1, &[&account_owner]) + .await + .unwrap_err(); + assert_eq!(error, client_error(TokenError::InvalidInstruction)); + + // Permissioned burn with the wrong permissioned authority fails. + let wrong_permissioned = Keypair::new(); + let ix_wrong = permissioned_burn_instruction::burn_checked( + &spl_token_2022_interface::id(), + &account, + &token.get_address(), + &account_owner.pubkey(), + &wrong_permissioned.pubkey(), + &[], + 1, + decimals, + ) + .unwrap(); + let error = token + .process_ixs(&[ix_wrong], &[&account_owner, &wrong_permissioned]) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidAccountData) + ))) + ); + + // Permissioned burn with the configured authority succeeds. + let ix_ok = permissioned_burn_instruction::burn_checked( + &spl_token_2022_interface::id(), + &account, + &token.get_address(), + &account_owner.pubkey(), + &authority.pubkey(), + &[], + 1, + decimals, + ) + .unwrap(); + token + .process_ixs(&[ix_ok], &[&account_owner, &authority]) + .await + .unwrap(); + + let account_after = token.get_account_info(&account).await.unwrap(); + assert_eq!(u64::from(account_after.base.amount), 1); + let mint_after = token.get_mint_info().await.unwrap(); + assert_eq!(u64::from(mint_after.base.supply), 1); +} diff --git a/program/src/processor.rs b/program/src/processor.rs index 7d192acf3..711751e1d 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1103,7 +1103,7 @@ impl Processor { let authority_info = next_account_info(account_info_iter)?; let mut mint_data = mint_info.data.borrow_mut(); - let mut mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; + let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; let permissioned_ext = mint.get_extension::(); let mut forward = vec![ From 1cd3ce68e6ae752d8e6e834a49dcda18a5329c39 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 28 Nov 2025 14:20:09 +0100 Subject: [PATCH 17/32] clean up & fix --- program/src/processor.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 711751e1d..038703637 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1134,8 +1134,6 @@ impl Processor { if Some(*approver_ai.key) != maybe_burn_authority { return Err(ProgramError::InvalidAccountData); } - - forward.push(approver_ai.clone()); } } @@ -1149,16 +1147,11 @@ impl Processor { let burn_accounts = forward; let account_info_iter = &mut burn_accounts.iter(); - let source_account_info = next_account_info(account_info_iter)?; - let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; let authority_info_data_len = authority_info.data_len(); let mut source_account_data = source_account_info.data.borrow_mut(); let source_account = PodStateWithExtensionsMut::::unpack(&mut source_account_data)?; - let mut mint_data = mint_info.data.borrow_mut(); - let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; if source_account.base.is_frozen() { return Err(TokenError::AccountFrozen.into()); From 56e1a938e8cfff0ec24100a6627d4a402b7bfdb1 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 28 Nov 2025 14:25:20 +0100 Subject: [PATCH 18/32] more cleanup --- program/src/processor.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 038703637..ee845f4dd 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1106,11 +1106,6 @@ impl Processor { let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; let permissioned_ext = mint.get_extension::(); - let mut forward = vec![ - source_account_info.clone(), - mint_info.clone(), - authority_info.clone(), - ]; match instruction_variant { BurnInstructionVariant::Standard(_) => { @@ -1137,16 +1132,10 @@ impl Processor { } } - let remaining_after = account_info_iter.as_slice(); - forward.extend_from_slice(remaining_after); - let instruction_variant = match instruction_variant { BurnInstructionVariant::Standard(v) | BurnInstructionVariant::Permissioned(v) => v, }; - let burn_accounts = forward; - let account_info_iter = &mut burn_accounts.iter(); - let authority_info_data_len = authority_info.data_len(); let mut source_account_data = source_account_info.data.borrow_mut(); From d51e9dda6f7ebb0460d4550e29acb284525a52fe Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 28 Nov 2025 15:35:54 +0100 Subject: [PATCH 19/32] js-legacy test --- .../js-legacy/src/extensions/extensionType.ts | 7 + clients/js-legacy/src/extensions/index.ts | 1 + .../src/extensions/permissionedBurn/index.ts | 2 + .../permissionedBurn/instructions.ts | 182 ++++++++++++++++++ .../src/extensions/permissionedBurn/state.ts | 27 +++ .../src/instructions/setAuthority.ts | 1 + clients/js-legacy/src/instructions/types.ts | 1 + .../test/e2e-2022/permissionedBurn.test.ts | 111 +++++++++++ 8 files changed, 332 insertions(+) create mode 100644 clients/js-legacy/src/extensions/permissionedBurn/index.ts create mode 100644 clients/js-legacy/src/extensions/permissionedBurn/instructions.ts create mode 100644 clients/js-legacy/src/extensions/permissionedBurn/state.ts create mode 100644 clients/js-legacy/test/e2e-2022/permissionedBurn.test.ts diff --git a/clients/js-legacy/src/extensions/extensionType.ts b/clients/js-legacy/src/extensions/extensionType.ts index e00b3eddf..e4842a5e9 100644 --- a/clients/js-legacy/src/extensions/extensionType.ts +++ b/clients/js-legacy/src/extensions/extensionType.ts @@ -18,6 +18,7 @@ import { MINT_CLOSE_AUTHORITY_SIZE } from './mintCloseAuthority.js'; import { NON_TRANSFERABLE_SIZE, NON_TRANSFERABLE_ACCOUNT_SIZE } from './nonTransferable.js'; import { PAUSABLE_CONFIG_SIZE, PAUSABLE_ACCOUNT_SIZE } from './pausable/index.js'; import { PERMANENT_DELEGATE_SIZE } from './permanentDelegate.js'; +import { PERMISSIONED_BURN_SIZE } from './permissionedBurn/state.js'; import { SCALED_UI_AMOUNT_CONFIG_SIZE } from './scaledUiAmount/index.js'; import { TRANSFER_FEE_AMOUNT_SIZE, TRANSFER_FEE_CONFIG_SIZE } from './transferFee/index.js'; import { TRANSFER_HOOK_ACCOUNT_SIZE, TRANSFER_HOOK_SIZE } from './transferHook/index.js'; @@ -53,6 +54,7 @@ export enum ExtensionType { ScaledUiAmountConfig = 25, PausableConfig = 26, PausableAccount = 27, + PermissionedBurn = 28, } export const TYPE_SIZE = 2; @@ -123,6 +125,8 @@ export function getTypeLen(e: ExtensionType): number { return PAUSABLE_CONFIG_SIZE; case ExtensionType.PausableAccount: return PAUSABLE_ACCOUNT_SIZE; + case ExtensionType.PermissionedBurn: + return PERMISSIONED_BURN_SIZE; case ExtensionType.TokenMetadata: throw Error(`Cannot get type length for variable extension type: ${e}`); default: @@ -148,6 +152,7 @@ export function isMintExtension(e: ExtensionType): boolean { case ExtensionType.TokenGroupMember: case ExtensionType.ScaledUiAmountConfig: case ExtensionType.PausableConfig: + case ExtensionType.PermissionedBurn: return true; case ExtensionType.Uninitialized: case ExtensionType.TransferFeeAmount: @@ -192,6 +197,7 @@ export function isAccountExtension(e: ExtensionType): boolean { case ExtensionType.TokenGroupMember: case ExtensionType.ScaledUiAmountConfig: case ExtensionType.PausableConfig: + case ExtensionType.PermissionedBurn: return false; default: throw Error(`Unknown extension type: ${e}`); @@ -230,6 +236,7 @@ export function getAccountTypeOfMintType(e: ExtensionType): ExtensionType { case ExtensionType.TokenGroupMember: case ExtensionType.ScaledUiAmountConfig: case ExtensionType.PausableAccount: + case ExtensionType.PermissionedBurn: return ExtensionType.Uninitialized; } } diff --git a/clients/js-legacy/src/extensions/index.ts b/clients/js-legacy/src/extensions/index.ts index 55a7b80ef..683248de5 100644 --- a/clients/js-legacy/src/extensions/index.ts +++ b/clients/js-legacy/src/extensions/index.ts @@ -17,3 +17,4 @@ export * from './transferFee/index.js'; export * from './permanentDelegate.js'; export * from './transferHook/index.js'; export * from './pausable/index.js'; +export * from './permissionedBurn/index.js'; diff --git a/clients/js-legacy/src/extensions/permissionedBurn/index.ts b/clients/js-legacy/src/extensions/permissionedBurn/index.ts new file mode 100644 index 000000000..8bf2a08d1 --- /dev/null +++ b/clients/js-legacy/src/extensions/permissionedBurn/index.ts @@ -0,0 +1,2 @@ +export * from './instructions.js'; +export * from './state.js'; diff --git a/clients/js-legacy/src/extensions/permissionedBurn/instructions.ts b/clients/js-legacy/src/extensions/permissionedBurn/instructions.ts new file mode 100644 index 000000000..03c5d4e55 --- /dev/null +++ b/clients/js-legacy/src/extensions/permissionedBurn/instructions.ts @@ -0,0 +1,182 @@ +import { struct, u8 } from '@solana/buffer-layout'; +import { publicKey, u64 } from '@solana/buffer-layout-utils'; +import type { PublicKey, Signer } from '@solana/web3.js'; +import { TransactionInstruction } from '@solana/web3.js'; +import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js'; +import { TokenUnsupportedInstructionError } from '../../errors.js'; +import { addSigners } from '../../instructions/internal.js'; +import { TokenInstruction } from '../../instructions/types.js'; + +export enum PermissionedBurnInstruction { + Initialize = 0, + Burn = 1, + BurnChecked = 2, +} + +interface InitializePermissionedBurnInstructionData { + instruction: TokenInstruction.PermissionedBurnExtension; + permissionedBurnInstruction: PermissionedBurnInstruction.Initialize; + authority: PublicKey; +} + +const initializePermissionedBurnInstructionData = struct([ + u8('instruction'), + u8('permissionedBurnInstruction'), + publicKey('authority'), +]); + +/** + * Construct a InitializePermissionedBurnConfig instruction + * + * @param mint Token mint account + * @param authority The permissioned burn mint's authority + * @param programId SPL Token program account + */ +export function createInitializePermissionedBurnInstruction( + mint: PublicKey, + authority: PublicKey, + programId = TOKEN_2022_PROGRAM_ID, +): TransactionInstruction { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + + const keys = [{ pubkey: mint, isSigner: false, isWritable: true }]; + const data = Buffer.alloc(initializePermissionedBurnInstructionData.span); + initializePermissionedBurnInstructionData.encode( + { + instruction: TokenInstruction.PermissionedBurnExtension, + permissionedBurnInstruction: PermissionedBurnInstruction.Initialize, + authority, + }, + data, + ); + + return new TransactionInstruction({ keys, programId, data }); +} + +interface PermissionedBurnInstructionData { + instruction: TokenInstruction.PermissionedBurnExtension; + permissionedBurnInstruction: PermissionedBurnInstruction.Burn; + amount: bigint; +} + +const permissionedBurnInstructionData = struct([ + u8('instruction'), + u8('permissionedBurnInstruction'), + u64('amount'), +]); + +/** + * Construct a permissioned burn instruction + * + * @param account Token account to update + * @param mint Token mint account + * @param owner The account's owner/delegate + * @param permissionedBurnAuthority The account's owner/delegate + * @param amount Amount to burn + * @param multiSigners The signer account(s) + * @param programId SPL Token program account + */ +export function createPermissionedBurnInstruction( + account: PublicKey, + mint: PublicKey, + owner: PublicKey, + permissionedBurnAuthority: PublicKey, + amount: number | bigint, + multiSigners: (Signer | PublicKey)[] = [], + programId = TOKEN_2022_PROGRAM_ID, +): TransactionInstruction { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + + const keys = addSigners( + [ + { pubkey: account, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: true }, + ], + owner, + multiSigners, + ); + + // permissioned burn authority comes after the owner/delegate and before any multisig signers + keys.splice(3, 0, { pubkey: permissionedBurnAuthority, isSigner: true, isWritable: false }); + + const data = Buffer.alloc(permissionedBurnInstructionData.span); + permissionedBurnInstructionData.encode( + { + instruction: TokenInstruction.PermissionedBurnExtension, + permissionedBurnInstruction: PermissionedBurnInstruction.Burn, + amount: BigInt(amount), + }, + data, + ); + + return new TransactionInstruction({ keys, programId, data }); +} + +interface PermissionedBurnCheckedInstructionData { + instruction: TokenInstruction.PermissionedBurnExtension; + permissionedBurnInstruction: PermissionedBurnInstruction.BurnChecked; + amount: bigint; + decimals: number; +} + +const permissionedBurnCheckedInstructionData = struct([ + u8('instruction'), + u8('permissionedBurnInstruction'), + u64('amount'), + u8('decimals'), +]); + +/** + * Construct a checked permissioned burn instruction + * + * @param account Token account to update + * @param mint Token mint account + * @param owner The account's owner/delegate + * @param permissionedBurnAuthority The account's owner/delegate + * @param amount Amount to burn + * @param decimals Number of the decimals of the mint + * @param multiSigners The signer account(s) + * @param programId SPL Token program account + */ +export function createPermissionedBurnCheckedInstruction( + account: PublicKey, + mint: PublicKey, + owner: PublicKey, + permissionedBurnAuthority: PublicKey, + amount: number | bigint, + decimals: number, + multiSigners: (Signer | PublicKey)[] = [], + programId = TOKEN_2022_PROGRAM_ID, +): TransactionInstruction { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + + const keys = addSigners( + [ + { pubkey: account, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: true }, + ], + owner, + multiSigners, + ); + + keys.splice(3, 0, { pubkey: permissionedBurnAuthority, isSigner: true, isWritable: false }); + + const data = Buffer.alloc(permissionedBurnCheckedInstructionData.span); + permissionedBurnCheckedInstructionData.encode( + { + instruction: TokenInstruction.PermissionedBurnExtension, + permissionedBurnInstruction: PermissionedBurnInstruction.BurnChecked, + amount: BigInt(amount), + decimals, + }, + data, + ); + + return new TransactionInstruction({ keys, programId, data }); +} diff --git a/clients/js-legacy/src/extensions/permissionedBurn/state.ts b/clients/js-legacy/src/extensions/permissionedBurn/state.ts new file mode 100644 index 000000000..0dc65e98d --- /dev/null +++ b/clients/js-legacy/src/extensions/permissionedBurn/state.ts @@ -0,0 +1,27 @@ +import { struct } from '@solana/buffer-layout'; +import { publicKey } from '@solana/buffer-layout-utils'; +import { PublicKey } from '@solana/web3.js'; +import type { Mint } from '../../state/mint.js'; +import { ExtensionType, getExtensionData } from '../extensionType.js'; + +/** Permissioned burn configuration as stored by the program */ +export interface PermissionedBurn { + authority: PublicKey | null; +} + +/** Buffer layout for de/serializing a permissioned burn config */ +export const PermissionedBurnLayout = struct<{ authority: PublicKey }>([publicKey('authority')]); + +export const PERMISSIONED_BURN_SIZE = PermissionedBurnLayout.span; + +export function getPermissionedBurn(mint: Mint): PermissionedBurn | null { + const extensionData = getExtensionData(ExtensionType.PermissionedBurn, mint.tlvData); + if (extensionData !== null) { + const { authority } = PermissionedBurnLayout.decode(extensionData); + return { + authority: authority.equals(PublicKey.default) ? null : authority, + }; + } else { + return null; + } +} diff --git a/clients/js-legacy/src/instructions/setAuthority.ts b/clients/js-legacy/src/instructions/setAuthority.ts index c37917147..d9c269887 100644 --- a/clients/js-legacy/src/instructions/setAuthority.ts +++ b/clients/js-legacy/src/instructions/setAuthority.ts @@ -32,6 +32,7 @@ export enum AuthorityType { GroupMemberPointer = 14, ScaledUiAmountConfig = 15, PausableConfig = 16, + PermissionedBurn = 17, } /** TODO: docs */ diff --git a/clients/js-legacy/src/instructions/types.ts b/clients/js-legacy/src/instructions/types.ts index 7a93b7c8b..7a1c199b6 100644 --- a/clients/js-legacy/src/instructions/types.ts +++ b/clients/js-legacy/src/instructions/types.ts @@ -45,4 +45,5 @@ export enum TokenInstruction { // ConfidentialMintBurnExtension = 42, ScaledUiAmountExtension = 43, PausableExtension = 44, + PermissionedBurnExtension = 46, } diff --git a/clients/js-legacy/test/e2e-2022/permissionedBurn.test.ts b/clients/js-legacy/test/e2e-2022/permissionedBurn.test.ts new file mode 100644 index 000000000..c6f3d1a24 --- /dev/null +++ b/clients/js-legacy/test/e2e-2022/permissionedBurn.test.ts @@ -0,0 +1,111 @@ +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +use(chaiAsPromised); + +import type { Connection, PublicKey, Signer } from '@solana/web3.js'; +import { Keypair, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js'; +import { + ExtensionType, + burn, + createAccount, + createInitializeMintInstruction, + createPermissionedBurnCheckedInstruction, + createInitializePermissionedBurnInstruction, + getMint, + getMintLen, + getPermissionedBurn, + mintTo, +} from '../../src'; +import { TEST_PROGRAM_ID, getConnection, newAccountWithLamports } from '../common'; + +const TEST_TOKEN_DECIMALS = 0; +const EXTENSIONS = [ExtensionType.PermissionedBurn]; + +describe('permissioned burn', () => { + let connection: Connection; + let payer: Signer; + let mint: PublicKey; + let mintAuthority: Keypair; + let permissionedAuthority: Keypair; + before(async () => { + connection = await getConnection(); + payer = await newAccountWithLamports(connection, 1_000_000_000); + mintAuthority = Keypair.generate(); + permissionedAuthority = Keypair.generate(); + }); + + beforeEach(async () => { + const mintKeypair = Keypair.generate(); + mint = mintKeypair.publicKey; + const mintLen = getMintLen(EXTENSIONS); + const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mint, + space: mintLen, + lamports, + programId: TEST_PROGRAM_ID, + }), + createInitializePermissionedBurnInstruction(mint, permissionedAuthority.publicKey, TEST_PROGRAM_ID), + createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, mintAuthority.publicKey, null, TEST_PROGRAM_ID), + ); + + await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair]); + }); + + it('initializes config', async () => { + const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); + const permissionedConfig = getPermissionedBurn(mintInfo); + expect(permissionedConfig).to.not.equal(null); + if (permissionedConfig !== null) { + expect(permissionedConfig.authority).to.eql(permissionedAuthority.publicKey); + } + }); + + it('enforces permissioned authority for burn', async () => { + const owner = Keypair.generate(); + const account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); + await mintTo(connection, payer, mint, account, mintAuthority, 2, [], undefined, TEST_PROGRAM_ID); + + await expect(burn(connection, payer, account, mint, owner, 1, [], undefined, TEST_PROGRAM_ID)).to.be.rejectedWith( + Error, + ); + + const wrongPermissioned = Keypair.generate(); + const badBurnTx = new Transaction().add( + createPermissionedBurnCheckedInstruction( + account, + mint, + owner.publicKey, + wrongPermissioned.publicKey, + 1, + TEST_TOKEN_DECIMALS, + [], + TEST_PROGRAM_ID, + ), + ); + await expect(sendAndConfirmTransaction(connection, badBurnTx, [payer, owner, wrongPermissioned])).to.be.rejectedWith( + Error, + ); + + const burnTx = new Transaction().add( + createPermissionedBurnCheckedInstruction( + account, + mint, + owner.publicKey, + permissionedAuthority.publicKey, + 1, + TEST_TOKEN_DECIMALS, + [], + TEST_PROGRAM_ID, + ), + ); + await sendAndConfirmTransaction(connection, burnTx, [payer, owner, permissionedAuthority]); + + const accountInfo = await connection.getTokenAccountBalance(account); + expect(accountInfo.value.uiAmount).to.eql(1); + const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); + expect(mintInfo.supply).to.eql(BigInt(1)); + }); +}); From 1f8cba9875d87140740deb9cc03a6c4ddded19f6 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 28 Nov 2025 16:06:34 +0100 Subject: [PATCH 20/32] add to cli --- clients/cli/src/clap_app.rs | 6 ++++++ clients/cli/src/command.rs | 6 ++++++ clients/cli/tests/command.rs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/clients/cli/src/clap_app.rs b/clients/cli/src/clap_app.rs index 86f2a23cc..524dd82dd 100644 --- a/clients/cli/src/clap_app.rs +++ b/clients/cli/src/clap_app.rs @@ -925,6 +925,12 @@ pub fn app<'a>( "Enable the mint authority to pause mint, burn, and transfer for this mint" ) ) + .arg( + Arg::with_name("enable_permissioned_burn") + .long("enable-permissioned-burn") + .takes_value(false) + .help("Require the configured permissioned burn authority for burning tokens") + ) .arg(multisig_signer_arg()) .nonce_args(true) .arg(memo_arg()) diff --git a/clients/cli/src/command.rs b/clients/cli/src/command.rs index 7163453fc..1bf75141b 100644 --- a/clients/cli/src/command.rs +++ b/clients/cli/src/command.rs @@ -268,6 +268,7 @@ async fn command_create_token( enable_transfer_hook: bool, ui_multiplier: Option, pausable: bool, + enable_permissioned_burn: bool, bulk_signers: Vec>, ) -> CommandResult { println_display( @@ -409,6 +410,10 @@ async fn command_create_token( extensions.push(ExtensionInitializationParams::PausableConfig { authority }); } + if enable_permissioned_burn { + extensions.push(ExtensionInitializationParams::PermissionedBurnConfig { authority }); + } + let res = token .create_mint( &authority, @@ -3804,6 +3809,7 @@ pub async fn process_command( arg_matches.is_present("enable_transfer_hook"), ui_multiplier, arg_matches.is_present("enable_pause"), + arg_matches.is_present("enable_permissioned_burn"), bulk_signers, ) .await diff --git a/clients/cli/tests/command.rs b/clients/cli/tests/command.rs index 5c64b6983..e5a2bfc4d 100644 --- a/clients/cli/tests/command.rs +++ b/clients/cli/tests/command.rs @@ -28,6 +28,7 @@ use { metadata_pointer::MetadataPointer, non_transferable::NonTransferable, pausable::PausableConfig, + permissioned_burn::PermissionedBurnConfig, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, transfer_hook::TransferHook, @@ -148,6 +149,7 @@ async fn main() { async_trial!(compute_budget, test_validator, payer), async_trial!(scaled_ui_amount, test_validator, payer), async_trial!(pause, test_validator, payer), + async_trial!(permissioned_burn, test_validator, payer), // GC messes with every other test, so have it on its own test validator async_trial!(gc, gc_test_validator, gc_payer), ]; @@ -4507,3 +4509,34 @@ async fn pause(test_validator: &TestValidator, payer: &Keypair) { let extension = test_mint.get_extension::().unwrap(); assert_eq!(Option::::from(extension.authority), None,); } + +async fn permissioned_burn(test_validator: &TestValidator, payer: &Keypair) { + let config = + test_config_with_default_signer(test_validator, payer, &spl_token_2022_interface::id()); + + let token = Keypair::new(); + let token_keypair_file = NamedTempFile::new().unwrap(); + write_keypair_file(&token, &token_keypair_file).unwrap(); + let token_pubkey = token.pubkey(); + + process_test_command( + &config, + payer, + &[ + "spl-token", + CommandName::CreateToken.into(), + token_keypair_file.path().to_str().unwrap(), + "--enable-permissioned-burn", + ], + ) + .await + .unwrap(); + + let account = config.rpc_client.get_account(&token_pubkey).await.unwrap(); + let test_mint = StateWithExtensionsOwned::::unpack(account.data).unwrap(); + let extension = test_mint.get_extension::().unwrap(); + assert_eq!( + Option::::from(extension.authority), + Some(payer.pubkey()) + ); +} From 4d7f325b1da4e9e0a2c0d1e316bad6cf2d6f09e2 Mon Sep 17 00:00:00 2001 From: Sergej Date: Sat, 29 Nov 2025 16:18:47 +0100 Subject: [PATCH 21/32] add to js-client --- .../js/src/generated/instructions/index.ts | 3 + .../initializePermissionedBurn.ts | 185 +++++++++++ .../instructions/permissionedBurn.ts | 290 +++++++++++++++++ .../instructions/permissionedBurnChecked.ts | 297 ++++++++++++++++++ clients/js/src/generated/types/extension.ts | 48 ++- .../js/src/generated/types/extensionType.ts | 1 + .../getInitializeInstructionsForExtensions.ts | 17 + .../initializePermissionedBurn.test.ts | 59 ++++ 8 files changed, 898 insertions(+), 2 deletions(-) create mode 100644 clients/js/src/generated/instructions/initializePermissionedBurn.ts create mode 100644 clients/js/src/generated/instructions/permissionedBurn.ts create mode 100644 clients/js/src/generated/instructions/permissionedBurnChecked.ts create mode 100644 clients/js/test/extensions/permissionedBurn/initializePermissionedBurn.test.ts diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index 8b526eaa4..5c7da2372 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -56,6 +56,7 @@ export * from './initializeMultisig'; export * from './initializeMultisig2'; export * from './initializeNonTransferableMint'; export * from './initializePausableConfig'; +export * from './initializePermissionedBurn'; export * from './initializePermanentDelegate'; export * from './initializeScaledUiAmountMint'; export * from './initializeTokenGroup'; @@ -66,6 +67,8 @@ export * from './initializeTransferHook'; export * from './mintTo'; export * from './mintToChecked'; export * from './pause'; +export * from './permissionedBurn'; +export * from './permissionedBurnChecked'; export * from './reallocate'; export * from './recoverNestedAssociatedToken'; export * from './removeTokenMetadataKey'; diff --git a/clients/js/src/generated/instructions/initializePermissionedBurn.ts b/clients/js/src/generated/instructions/initializePermissionedBurn.ts new file mode 100644 index 000000000..e5f1b336f --- /dev/null +++ b/clients/js/src/generated/instructions/initializePermissionedBurn.ts @@ -0,0 +1,185 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + getAddressDecoder, + getAddressEncoder, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyUint8Array, + type WritableAccount, +} from '@solana/kit'; +import { TOKEN_2022_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const INITIALIZE_PERMISSIONED_BURN_DISCRIMINATOR = 46; + +export function getInitializePermissionedBurnDiscriminatorBytes() { + return getU8Encoder().encode(INITIALIZE_PERMISSIONED_BURN_DISCRIMINATOR); +} + +export const INITIALIZE_PERMISSIONED_BURN_PERMISSIONED_BURN_DISCRIMINATOR = 0; + +export function getInitializePermissionedBurnPermissionedBurnDiscriminatorBytes() { + return getU8Encoder().encode( + INITIALIZE_PERMISSIONED_BURN_PERMISSIONED_BURN_DISCRIMINATOR + ); +} + +export type InitializePermissionedBurnInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountMint extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountMint extends string + ? WritableAccount + : TAccountMint, + ...TRemainingAccounts, + ] + >; + +export type InitializePermissionedBurnInstructionData = { + discriminator: number; + permissionedBurnDiscriminator: number; + /** The public key for the account that is required for token burning. */ + authority: Address; +}; + +export type InitializePermissionedBurnInstructionDataArgs = { + /** The public key for the account that is required for token burning. */ + authority: Address; +}; + +export function getInitializePermissionedBurnInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['permissionedBurnDiscriminator', getU8Encoder()], + ['authority', getAddressEncoder()], + ]), + (value) => ({ + ...value, + discriminator: INITIALIZE_PERMISSIONED_BURN_DISCRIMINATOR, + permissionedBurnDiscriminator: + INITIALIZE_PERMISSIONED_BURN_PERMISSIONED_BURN_DISCRIMINATOR, + }) + ); +} + +export function getInitializePermissionedBurnInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['permissionedBurnDiscriminator', getU8Decoder()], + ['authority', getAddressDecoder()], + ]); +} + +export function getInitializePermissionedBurnInstructionDataCodec(): FixedSizeCodec< + InitializePermissionedBurnInstructionDataArgs, + InitializePermissionedBurnInstructionData +> { + return combineCodec( + getInitializePermissionedBurnInstructionDataEncoder(), + getInitializePermissionedBurnInstructionDataDecoder() + ); +} + +export type InitializePermissionedBurnInput< + TAccountMint extends string = string, +> = { + /** The mint to initialize. */ + mint: Address; + /** The public key for the account that is required for token burning. */ + authority: InitializePermissionedBurnInstructionDataArgs['authority']; +}; + +export function getInitializePermissionedBurnInstruction< + TAccountMint extends string, + TProgramAddress extends Address = typeof TOKEN_2022_PROGRAM_ADDRESS, +>( + input: InitializePermissionedBurnInput, + config?: { programAddress?: TProgramAddress } +): InitializePermissionedBurnInstruction { + // Program address. + const programAddress = config?.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + mint: { value: input.mint ?? null, isWritable: true }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [getAccountMeta(accounts.mint)], + data: getInitializePermissionedBurnInstructionDataEncoder().encode( + args as InitializePermissionedBurnInstructionDataArgs + ), + programAddress, + } as InitializePermissionedBurnInstruction); +} + +export type ParsedInitializePermissionedBurnInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + /** The mint to initialize. */ + mint: TAccountMetas[0]; + }; + data: InitializePermissionedBurnInstructionData; +}; + +export function parseInitializePermissionedBurnInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedInitializePermissionedBurnInstruction { + if (instruction.accounts.length < 1) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { mint: getNextAccount() }, + data: getInitializePermissionedBurnInstructionDataDecoder().decode( + instruction.data + ), + }; +} diff --git a/clients/js/src/generated/instructions/permissionedBurn.ts b/clients/js/src/generated/instructions/permissionedBurn.ts new file mode 100644 index 000000000..0cd15f0e1 --- /dev/null +++ b/clients/js/src/generated/instructions/permissionedBurn.ts @@ -0,0 +1,290 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + AccountRole, + combineCodec, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyAccount, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit'; +import { TOKEN_2022_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const PERMISSIONED_BURN_DISCRIMINATOR = 46; + +export function getPermissionedBurnDiscriminatorBytes() { + return getU8Encoder().encode(PERMISSIONED_BURN_DISCRIMINATOR); +} + +export const PERMISSIONED_BURN_PERMISSIONED_BURN_DISCRIMINATOR = 1; + +export function getPermissionedBurnPermissionedBurnDiscriminatorBytes() { + return getU8Encoder().encode( + PERMISSIONED_BURN_PERMISSIONED_BURN_DISCRIMINATOR + ); +} + +export type PermissionedBurnInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountAccount extends string | AccountMeta = string, + TAccountMint extends string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, + TAccountPermissionedBurnAuthority extends + | string + | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountAccount extends string + ? WritableAccount + : TAccountAccount, + TAccountMint extends string + ? WritableAccount + : TAccountMint, + TAccountAuthority extends string + ? ReadonlyAccount + : TAccountAuthority, + TAccountPermissionedBurnAuthority extends string + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountPermissionedBurnAuthority, + ...TRemainingAccounts, + ] + >; + +export type PermissionedBurnInstructionData = { + discriminator: number; + permissionedBurnDiscriminator: number; + /** The amount of tokens to burn. */ + amount: bigint; +}; + +export type PermissionedBurnInstructionDataArgs = { + /** The amount of tokens to burn. */ + amount: number | bigint; +}; + +export function getPermissionedBurnInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['permissionedBurnDiscriminator', getU8Encoder()], + ['amount', getU64Encoder()], + ]), + (value) => ({ + ...value, + discriminator: PERMISSIONED_BURN_DISCRIMINATOR, + permissionedBurnDiscriminator: + PERMISSIONED_BURN_PERMISSIONED_BURN_DISCRIMINATOR, + }) + ); +} + +export function getPermissionedBurnInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['permissionedBurnDiscriminator', getU8Decoder()], + ['amount', getU64Decoder()], + ]); +} + +export function getPermissionedBurnInstructionDataCodec(): FixedSizeCodec< + PermissionedBurnInstructionDataArgs, + PermissionedBurnInstructionData +> { + return combineCodec( + getPermissionedBurnInstructionDataEncoder(), + getPermissionedBurnInstructionDataDecoder() + ); +} + +export type PermissionedBurnInput< + TAccountAccount extends string = string, + TAccountMint extends string = string, + TAccountAuthority extends string = string, + TAccountPermissionedBurnAuthority extends string = string, +> = { + /** The source account to burn from. */ + account: Address; + /** The token mint. */ + mint: Address; + /** The account's owner/delegate or its multisignature account. */ + authority: + | Address + | TransactionSigner; + /** + * Authority configured on the mint that must sign any permissioned burn + * instruction. + */ + permissionedBurnAuthority: + | Address + | TransactionSigner; + amount: PermissionedBurnInstructionDataArgs['amount']; + multiSigners?: Array; +}; + +export function getPermissionedBurnInstruction< + TAccountAccount extends string, + TAccountMint extends string, + TAccountAuthority extends string, + TAccountPermissionedBurnAuthority extends string, + TProgramAddress extends Address = typeof TOKEN_2022_PROGRAM_ADDRESS, +>( + input: PermissionedBurnInput< + TAccountAccount, + TAccountMint, + TAccountAuthority, + TAccountPermissionedBurnAuthority + >, + config?: { programAddress?: TProgramAddress } +): PermissionedBurnInstruction< + TProgramAddress, + TAccountAccount, + TAccountMint, + (typeof input)['authority'] extends TransactionSigner + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountAuthority, + (typeof input)['permissionedBurnAuthority'] extends TransactionSigner + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountPermissionedBurnAuthority +> { + // Program address. + const programAddress = config?.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + account: { value: input.account ?? null, isWritable: true }, + mint: { value: input.mint ?? null, isWritable: true }, + authority: { value: input.authority ?? null, isWritable: false }, + permissionedBurnAuthority: { + value: input.permissionedBurnAuthority ?? null, + isWritable: false, + }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + // Remaining accounts. + const remainingAccounts: AccountMeta[] = (args.multiSigners ?? []).map( + (signer) => ({ + address: signer.address, + role: AccountRole.READONLY_SIGNER, + signer, + }) + ); + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.account), + getAccountMeta(accounts.mint), + getAccountMeta(accounts.authority), + getAccountMeta(accounts.permissionedBurnAuthority), + ...remainingAccounts, + ], + data: getPermissionedBurnInstructionDataEncoder().encode( + args as PermissionedBurnInstructionDataArgs + ), + programAddress, + } as PermissionedBurnInstruction< + TProgramAddress, + TAccountAccount, + TAccountMint, + (typeof input)['authority'] extends TransactionSigner + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountAuthority, + (typeof input)['permissionedBurnAuthority'] extends TransactionSigner + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountPermissionedBurnAuthority + >); +} + +export type ParsedPermissionedBurnInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + /** The source account to burn from. */ + account: TAccountMetas[0]; + /** The token mint. */ + mint: TAccountMetas[1]; + /** The account's owner/delegate or its multisignature account. */ + authority: TAccountMetas[2]; + /** + * Authority configured on the mint that must sign any permissioned burn + * instruction. + */ + permissionedBurnAuthority: TAccountMetas[3]; + multiSigners: TAccountMetas[4][]; + }; + data: PermissionedBurnInstructionData; +}; + +export function parsePermissionedBurnInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedPermissionedBurnInstruction { + if (instruction.accounts.length < 4) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + account: getNextAccount(), + mint: getNextAccount(), + authority: getNextAccount(), + permissionedBurnAuthority: getNextAccount(), + multiSigners: instruction.accounts.slice(4) as TAccountMetas[4][], + }, + data: getPermissionedBurnInstructionDataDecoder().decode( + instruction.data + ), + }; +} diff --git a/clients/js/src/generated/instructions/permissionedBurnChecked.ts b/clients/js/src/generated/instructions/permissionedBurnChecked.ts new file mode 100644 index 000000000..356f2adbb --- /dev/null +++ b/clients/js/src/generated/instructions/permissionedBurnChecked.ts @@ -0,0 +1,297 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + AccountRole, + combineCodec, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyAccount, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit'; +import { TOKEN_2022_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const PERMISSIONED_BURN_CHECKED_DISCRIMINATOR = 46; + +export function getPermissionedBurnCheckedDiscriminatorBytes() { + return getU8Encoder().encode(PERMISSIONED_BURN_CHECKED_DISCRIMINATOR); +} + +export const PERMISSIONED_BURN_CHECKED_PERMISSIONED_BURN_DISCRIMINATOR = 2; + +export function getPermissionedBurnCheckedPermissionedBurnDiscriminatorBytes() { + return getU8Encoder().encode( + PERMISSIONED_BURN_CHECKED_PERMISSIONED_BURN_DISCRIMINATOR + ); +} + +export type PermissionedBurnCheckedInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountAccount extends string | AccountMeta = string, + TAccountMint extends string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, + TAccountPermissionedBurnAuthority extends + | string + | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountAccount extends string + ? WritableAccount + : TAccountAccount, + TAccountMint extends string + ? WritableAccount + : TAccountMint, + TAccountAuthority extends string + ? ReadonlyAccount + : TAccountAuthority, + TAccountPermissionedBurnAuthority extends string + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountPermissionedBurnAuthority, + ...TRemainingAccounts, + ] + >; + +export type PermissionedBurnCheckedInstructionData = { + discriminator: number; + permissionedBurnDiscriminator: number; + /** The amount of tokens to burn. */ + amount: bigint; + /** Expected number of base 10 digits to the right of the decimal place. */ + decimals: number; +}; + +export type PermissionedBurnCheckedInstructionDataArgs = { + /** The amount of tokens to burn. */ + amount: number | bigint; + /** Expected number of base 10 digits to the right of the decimal place. */ + decimals: number; +}; + +export function getPermissionedBurnCheckedInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['permissionedBurnDiscriminator', getU8Encoder()], + ['amount', getU64Encoder()], + ['decimals', getU8Encoder()], + ]), + (value) => ({ + ...value, + discriminator: PERMISSIONED_BURN_CHECKED_DISCRIMINATOR, + permissionedBurnDiscriminator: + PERMISSIONED_BURN_CHECKED_PERMISSIONED_BURN_DISCRIMINATOR, + }) + ); +} + +export function getPermissionedBurnCheckedInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['permissionedBurnDiscriminator', getU8Decoder()], + ['amount', getU64Decoder()], + ['decimals', getU8Decoder()], + ]); +} + +export function getPermissionedBurnCheckedInstructionDataCodec(): FixedSizeCodec< + PermissionedBurnCheckedInstructionDataArgs, + PermissionedBurnCheckedInstructionData +> { + return combineCodec( + getPermissionedBurnCheckedInstructionDataEncoder(), + getPermissionedBurnCheckedInstructionDataDecoder() + ); +} + +export type PermissionedBurnCheckedInput< + TAccountAccount extends string = string, + TAccountMint extends string = string, + TAccountAuthority extends string = string, + TAccountPermissionedBurnAuthority extends string = string, +> = { + /** The source account to burn from. */ + account: Address; + /** The token mint. */ + mint: Address; + /** The account's owner/delegate or its multisignature account. */ + authority: + | Address + | TransactionSigner; + /** + * Authority configured on the mint that must sign any permissioned burn + * instruction. + */ + permissionedBurnAuthority: + | Address + | TransactionSigner; + amount: PermissionedBurnCheckedInstructionDataArgs['amount']; + decimals: PermissionedBurnCheckedInstructionDataArgs['decimals']; + multiSigners?: Array; +}; + +export function getPermissionedBurnCheckedInstruction< + TAccountAccount extends string, + TAccountMint extends string, + TAccountAuthority extends string, + TAccountPermissionedBurnAuthority extends string, + TProgramAddress extends Address = typeof TOKEN_2022_PROGRAM_ADDRESS, +>( + input: PermissionedBurnCheckedInput< + TAccountAccount, + TAccountMint, + TAccountAuthority, + TAccountPermissionedBurnAuthority + >, + config?: { programAddress?: TProgramAddress } +): PermissionedBurnCheckedInstruction< + TProgramAddress, + TAccountAccount, + TAccountMint, + (typeof input)['authority'] extends TransactionSigner + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountAuthority, + (typeof input)['permissionedBurnAuthority'] extends TransactionSigner + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountPermissionedBurnAuthority +> { + // Program address. + const programAddress = config?.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + account: { value: input.account ?? null, isWritable: true }, + mint: { value: input.mint ?? null, isWritable: true }, + authority: { value: input.authority ?? null, isWritable: false }, + permissionedBurnAuthority: { + value: input.permissionedBurnAuthority ?? null, + isWritable: false, + }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + // Remaining accounts. + const remainingAccounts: AccountMeta[] = (args.multiSigners ?? []).map( + (signer) => ({ + address: signer.address, + role: AccountRole.READONLY_SIGNER, + signer, + }) + ); + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.account), + getAccountMeta(accounts.mint), + getAccountMeta(accounts.authority), + getAccountMeta(accounts.permissionedBurnAuthority), + ...remainingAccounts, + ], + data: getPermissionedBurnCheckedInstructionDataEncoder().encode( + args as PermissionedBurnCheckedInstructionDataArgs + ), + programAddress, + } as PermissionedBurnCheckedInstruction< + TProgramAddress, + TAccountAccount, + TAccountMint, + (typeof input)['authority'] extends TransactionSigner + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountAuthority, + (typeof input)['permissionedBurnAuthority'] extends TransactionSigner + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountPermissionedBurnAuthority + >); +} + +export type ParsedPermissionedBurnCheckedInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + /** The source account to burn from. */ + account: TAccountMetas[0]; + /** The token mint. */ + mint: TAccountMetas[1]; + /** The account's owner/delegate or its multisignature account. */ + authority: TAccountMetas[2]; + /** + * Authority configured on the mint that must sign any permissioned burn + * instruction. + */ + permissionedBurnAuthority: TAccountMetas[3]; + multiSigners: TAccountMetas[4][]; + }; + data: PermissionedBurnCheckedInstructionData; +}; + +export function parsePermissionedBurnCheckedInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedPermissionedBurnCheckedInstruction { + if (instruction.accounts.length < 4) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + account: getNextAccount(), + mint: getNextAccount(), + authority: getNextAccount(), + permissionedBurnAuthority: getNextAccount(), + multiSigners: instruction.accounts.slice(4) as TAccountMetas[4][], + }, + data: getPermissionedBurnCheckedInstructionDataDecoder().decode( + instruction.data + ), + }; +} diff --git a/clients/js/src/generated/types/extension.ts b/clients/js/src/generated/types/extension.ts index bcad0205e..df4e59d3a 100644 --- a/clients/js/src/generated/types/extension.ts +++ b/clients/js/src/generated/types/extension.ts @@ -275,7 +275,8 @@ export type Extension = newMultiplier: number; } | { __kind: 'PausableConfig'; authority: Option
; paused: boolean } - | { __kind: 'PausableAccount' }; + | { __kind: 'PausableAccount' } + | { __kind: 'PermissionedBurn'; authority: Option
}; export type ExtensionArgs = | { __kind: 'Uninitialized' } @@ -492,7 +493,8 @@ export type ExtensionArgs = authority: OptionOrNullable
; paused: boolean; } - | { __kind: 'PausableAccount' }; + | { __kind: 'PausableAccount' } + | { __kind: 'PermissionedBurn'; authority: OptionOrNullable
}; export function getExtensionEncoder(): Encoder { return getDiscriminatedUnionEncoder( @@ -816,6 +818,21 @@ export function getExtensionEncoder(): Encoder { ), ], ['PausableAccount', getUnitEncoder()], + [ + 'PermissionedBurn', + addEncoderSizePrefix( + getStructEncoder([ + [ + 'authority', + getOptionEncoder(getAddressEncoder(), { + prefix: null, + noneValue: 'zeroes', + }), + ], + ]), + getU16Encoder() + ), + ], ], { size: getU16Encoder() } ); @@ -1143,6 +1160,21 @@ export function getExtensionDecoder(): Decoder { ), ], ['PausableAccount', getUnitDecoder()], + [ + 'PermissionedBurn', + addDecoderSizePrefix( + getStructDecoder([ + [ + 'authority', + getOptionDecoder(getAddressDecoder(), { + prefix: null, + noneValue: 'zeroes', + }), + ], + ]), + getU16Decoder() + ), + ], ], { size: getU16Decoder() } ); @@ -1390,6 +1422,18 @@ export function extension( export function extension( kind: 'PausableAccount' ): GetDiscriminatedUnionVariant; +export function extension( + kind: 'PermissionedBurn', + data: GetDiscriminatedUnionVariantContent< + ExtensionArgs, + '__kind', + 'PermissionedBurn' + > +): GetDiscriminatedUnionVariant< + ExtensionArgs, + '__kind', + 'PermissionedBurn' +>; export function extension( kind: K, data?: Data diff --git a/clients/js/src/generated/types/extensionType.ts b/clients/js/src/generated/types/extensionType.ts index 7b65d198c..9e3ed7941 100644 --- a/clients/js/src/generated/types/extensionType.ts +++ b/clients/js/src/generated/types/extensionType.ts @@ -50,6 +50,7 @@ export enum ExtensionType { TokenGroup, GroupMemberPointer, TokenGroupMember, + PermissionedBurn, } export type ExtensionTypeArgs = ExtensionType; diff --git a/clients/js/src/getInitializeInstructionsForExtensions.ts b/clients/js/src/getInitializeInstructionsForExtensions.ts index f7d13e5a3..c0e02bb3a 100644 --- a/clients/js/src/getInitializeInstructionsForExtensions.ts +++ b/clients/js/src/getInitializeInstructionsForExtensions.ts @@ -28,6 +28,7 @@ import { getInitializeScaledUiAmountMintInstruction, getInitializeConfidentialTransferFeeInstruction, getInitializePausableConfigInstruction, + getInitializePermissionedBurnInstruction, } from './generated'; /** @@ -97,6 +98,22 @@ export function getPreInitializeInstructionsForMintExtensions( authority: extension.authority, }), ]; + case 'PermissionedBurn': { + const authority = isOption(extension.authority) + ? extension.authority + : wrapNullable(extension.authority); + if (isNone(authority)) { + throw new Error( + 'PermissionedBurn extension requires a permissioned burn authority' + ); + } + return [ + getInitializePermissionedBurnInstruction({ + mint, + authority: authority.value, + }), + ]; + } case 'GroupPointer': return [ getInitializeGroupPointerInstruction({ diff --git a/clients/js/test/extensions/permissionedBurn/initializePermissionedBurn.test.ts b/clients/js/test/extensions/permissionedBurn/initializePermissionedBurn.test.ts new file mode 100644 index 000000000..4ffecda7f --- /dev/null +++ b/clients/js/test/extensions/permissionedBurn/initializePermissionedBurn.test.ts @@ -0,0 +1,59 @@ +import { Account, generateKeyPairSigner, some } from '@solana/kit'; +import test from 'ava'; +import { + Mint, + extension, + fetchMint, + getInitializePermissionedBurnInstruction, +} from '../../../src'; +import { + createDefaultSolanaClient, + generateKeyPairSignerWithSol, + getCreateMintInstructions, + sendAndConfirmInstructions, +} from '../../_setup'; + +test('it initializes a mint with permissioned burn', async (t) => { + // Given a fresh client and signers + const client = createDefaultSolanaClient(); + const [authority, mint, permissionedBurnAuthority] = await Promise.all([ + generateKeyPairSignerWithSol(client), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + + // And a permissioned burn extension + const permissionedBurnExtension = extension('PermissionedBurn', { + authority: some(permissionedBurnAuthority.address), + }); + + // When we create and initialize a mint account with this extension + const [createMintInstruction, initMintInstruction] = + await getCreateMintInstructions({ + authority: authority.address, + client, + extensions: [permissionedBurnExtension], + mint, + payer: authority, + }); + + await sendAndConfirmInstructions(client, authority, [ + createMintInstruction, + getInitializePermissionedBurnInstruction({ + mint: mint.address, + authority: permissionedBurnAuthority.address, + }), + initMintInstruction, + ]); + + // Then we expect the mint account to exist with the permissioned burn config + const mintAccount = await fetchMint(client.rpc, mint.address); + t.like(mintAccount, >{ + address: mint.address, + data: { + mintAuthority: some(authority.address), + isInitialized: true, + extensions: some([permissionedBurnExtension]), + }, + }); +}); From 0aa93da4e20c6bce94ff23e7005243e61e5cd538 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 5 Dec 2025 17:50:00 +0100 Subject: [PATCH 22/32] change order & separate enums --- .../permissionedBurn/instructions.ts | 11 ++--- .../instructions/permissionedBurn.ts | 12 ++--- .../instructions/permissionedBurnChecked.ts | 12 ++--- .../permissioned_burn/instruction.rs | 24 +++++----- .../extension/permissioned_burn/processor.rs | 8 ++-- program/src/processor.rs | 44 ++++++++++++------- 6 files changed, 60 insertions(+), 51 deletions(-) diff --git a/clients/js-legacy/src/extensions/permissionedBurn/instructions.ts b/clients/js-legacy/src/extensions/permissionedBurn/instructions.ts index 03c5d4e55..8ab169736 100644 --- a/clients/js-legacy/src/extensions/permissionedBurn/instructions.ts +++ b/clients/js-legacy/src/extensions/permissionedBurn/instructions.ts @@ -73,7 +73,7 @@ const permissionedBurnInstructionData = struct( * @param account Token account to update * @param mint Token mint account * @param owner The account's owner/delegate - * @param permissionedBurnAuthority The account's owner/delegate + * @param permissionedBurnAuthority Authority configured on the mint for permissioned burns * @param amount Amount to burn * @param multiSigners The signer account(s) * @param programId SPL Token program account @@ -95,14 +95,12 @@ export function createPermissionedBurnInstruction( [ { pubkey: account, isSigner: false, isWritable: true }, { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: permissionedBurnAuthority, isSigner: true, isWritable: false }, ], owner, multiSigners, ); - // permissioned burn authority comes after the owner/delegate and before any multisig signers - keys.splice(3, 0, { pubkey: permissionedBurnAuthority, isSigner: true, isWritable: false }); - const data = Buffer.alloc(permissionedBurnInstructionData.span); permissionedBurnInstructionData.encode( { @@ -136,7 +134,7 @@ const permissionedBurnCheckedInstructionData = struct { @@ -67,9 +68,10 @@ pub(crate) fn process_instruction( program_id, accounts, data.amount.into(), - BurnInstructionVariant::Permissioned(InstructionVariant::Checked { + BurnInstructionVariant::Permissioned, + InstructionVariant::Checked { decimals: data.decimals, - }), + }, ) } } diff --git a/program/src/processor.rs b/program/src/processor.rs index ee845f4dd..23db85087 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -87,8 +87,8 @@ pub(crate) enum InstructionVariant { /// /// Permissioned variants require the extra authority to sign. pub(crate) enum BurnInstructionVariant { - Standard(InstructionVariant), - Permissioned(InstructionVariant), + Standard, + Permissioned, } /// Program state handler. @@ -1094,32 +1094,46 @@ impl Processor { program_id: &Pubkey, accounts: &[AccountInfo], amount: u64, - instruction_variant: BurnInstructionVariant, + burn_variant: BurnInstructionVariant, + instruction_variant: InstructionVariant, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let source_account_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; + let (permissioned_burn_authority_info, authority_info) = + match burn_variant { + BurnInstructionVariant::Permissioned => { + let permissioned_burn_authority_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + (Some(permissioned_burn_authority_info), authority_info) + } + BurnInstructionVariant::Standard => { + (None, next_account_info(account_info_iter)?) + } + }; + + let authority_info_data_len = authority_info.data_len(); let mut mint_data = mint_info.data.borrow_mut(); let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; let permissioned_ext = mint.get_extension::(); - match instruction_variant { - BurnInstructionVariant::Standard(_) => { + match burn_variant { + BurnInstructionVariant::Standard => { // Standard burns cannot be used when the permissioned burn // extension is present. if permissioned_ext.is_ok() { return Err(TokenError::InvalidInstruction.into()); } } - BurnInstructionVariant::Permissioned(_) => { + BurnInstructionVariant::Permissioned => { let ext = permissioned_ext.map_err(|_| TokenError::InvalidInstruction)?; // Pull the required extra signer from the accounts - let approver_ai = next_account_info(account_info_iter)?; + let approver_ai = permissioned_burn_authority_info + .ok_or(ProgramError::NotEnoughAccountKeys)?; if !approver_ai.is_signer { return Err(ProgramError::MissingRequiredSignature); @@ -1132,12 +1146,6 @@ impl Processor { } } - let instruction_variant = match instruction_variant { - BurnInstructionVariant::Standard(v) | BurnInstructionVariant::Permissioned(v) => v, - }; - - let authority_info_data_len = authority_info.data_len(); - let mut source_account_data = source_account_info.data.borrow_mut(); let source_account = PodStateWithExtensionsMut::::unpack(&mut source_account_data)?; @@ -1801,7 +1809,8 @@ impl Processor { program_id, accounts, data.amount.into(), - BurnInstructionVariant::Standard(InstructionVariant::Unchecked), + BurnInstructionVariant::Standard, + InstructionVariant::Unchecked, ) } PodTokenInstruction::CloseAccount => { @@ -1859,9 +1868,10 @@ impl Processor { program_id, accounts, data.amount.into(), - BurnInstructionVariant::Standard(InstructionVariant::Checked { + BurnInstructionVariant::Standard, + InstructionVariant::Checked { decimals: data.decimals, - }), + }, ) } PodTokenInstruction::SyncNative => { From 7d9cea15b21f8eb9c11d28e05fbcffc152be6374 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 5 Dec 2025 17:59:49 +0100 Subject: [PATCH 23/32] leftover ordering update & authority update test --- .../rust-legacy/tests/permissioned_burn.rs | 58 ++++++++++++++++++- .../permissioned_burn/instruction.rs | 4 +- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/clients/rust-legacy/tests/permissioned_burn.rs b/clients/rust-legacy/tests/permissioned_burn.rs index 539d06614..4c2644cd2 100644 --- a/clients/rust-legacy/tests/permissioned_burn.rs +++ b/clients/rust-legacy/tests/permissioned_burn.rs @@ -8,6 +8,7 @@ use { }, spl_token_2022_interface::{ error::TokenError, + instruction::AuthorityType, extension::{ permissioned_burn::{ instruction as permissioned_burn_instruction, PermissionedBurnConfig, @@ -51,6 +52,7 @@ async fn success_initialize() { async fn permissioned_burn_enforced() { let mut context = TestContext::new().await; let authority = Keypair::new(); + let new_authority = Keypair::new(); context .init_token_with_mint(vec![ ExtensionInitializationParams::PermissionedBurnConfig { @@ -93,8 +95,8 @@ async fn permissioned_burn_enforced() { &spl_token_2022_interface::id(), &account, &token.get_address(), - &account_owner.pubkey(), &wrong_permissioned.pubkey(), + &account_owner.pubkey(), &[], 1, decimals, @@ -116,8 +118,8 @@ async fn permissioned_burn_enforced() { &spl_token_2022_interface::id(), &account, &token.get_address(), - &account_owner.pubkey(), &authority.pubkey(), + &account_owner.pubkey(), &[], 1, decimals, @@ -132,4 +134,56 @@ async fn permissioned_burn_enforced() { assert_eq!(u64::from(account_after.base.amount), 1); let mint_after = token.get_mint_info().await.unwrap(); assert_eq!(u64::from(mint_after.base.supply), 1); + + // Update permissioned burn authority and ensure new authority is enforced. + token + .set_authority( + token.get_address(), + &authority.pubkey(), + Some(&new_authority.pubkey()), + AuthorityType::PermissionedBurn, + &[&authority], + ) + .await + .unwrap(); + + // Old authority should no longer work. + let ix_old = permissioned_burn_instruction::burn_checked( + &spl_token_2022_interface::id(), + &account, + &token.get_address(), + &authority.pubkey(), + &account_owner.pubkey(), + &[], + 1, + decimals, + ) + .unwrap(); + let err_old = token + .process_ixs(&[ix_old], &[&account_owner, &authority]) + .await + .unwrap_err(); + assert_eq!( + err_old, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError(0, InstructionError::InvalidAccountData) + ))) + ); + + // New authority should succeed. + let ix_new = permissioned_burn_instruction::burn_checked( + &spl_token_2022_interface::id(), + &account, + &token.get_address(), + &new_authority.pubkey(), + &account_owner.pubkey(), + &[], + 1, + decimals, + ) + .unwrap(); + token + .process_ixs(&[ix_new], &[&account_owner, &new_authority]) + .await + .unwrap(); } diff --git a/interface/src/extension/permissioned_burn/instruction.rs b/interface/src/extension/permissioned_burn/instruction.rs index e85912152..6f810947c 100644 --- a/interface/src/extension/permissioned_burn/instruction.rs +++ b/interface/src/extension/permissioned_burn/instruction.rs @@ -114,8 +114,8 @@ pub fn burn( token_program_id: &Pubkey, account: &Pubkey, mint: &Pubkey, - authority: &Pubkey, permissioned_burn_authority: &Pubkey, + authority: &Pubkey, signer_pubkeys: &[&Pubkey], amount: u64, ) -> Result { @@ -153,8 +153,8 @@ pub fn burn_checked( token_program_id: &Pubkey, account: &Pubkey, mint: &Pubkey, - authority: &Pubkey, permissioned_burn_authority: &Pubkey, + authority: &Pubkey, signer_pubkeys: &[&Pubkey], amount: u64, decimals: u8, From 1adec9d16634549b06852407840e9b07cc303593 Mon Sep 17 00:00:00 2001 From: Sergej Date: Fri, 5 Dec 2025 21:48:57 +0100 Subject: [PATCH 24/32] --permissioned-burn-authority --- clients/cli/src/clap_app.rs | 11 ++++++++ clients/cli/src/command.rs | 26 +++++++++++++++++-- clients/cli/tests/command.rs | 5 +++- .../rust-legacy/tests/permissioned_burn.rs | 2 +- program/src/processor.rs | 23 +++++++--------- 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/clients/cli/src/clap_app.rs b/clients/cli/src/clap_app.rs index 524dd82dd..04f715535 100644 --- a/clients/cli/src/clap_app.rs +++ b/clients/cli/src/clap_app.rs @@ -245,6 +245,7 @@ pub enum CliAuthorityType { Group, ScaledUiAmount, Pause, + PermissionedBurn, } impl TryFrom for AuthorityType { type Error = Error; @@ -277,6 +278,7 @@ impl TryFrom for AuthorityType { } CliAuthorityType::ScaledUiAmount => Ok(AuthorityType::ScaledUiAmount), CliAuthorityType::Pause => Ok(AuthorityType::Pause), + CliAuthorityType::PermissionedBurn => Ok(AuthorityType::PermissionedBurn), } } } @@ -931,6 +933,15 @@ pub fn app<'a>( .takes_value(false) .help("Require the configured permissioned burn authority for burning tokens") ) + .arg( + Arg::with_name("permissioned_burn_authority") + .long("permissioned-burn-authority") + .validator(|s| is_valid_signer(s)) + .value_name("SIGNER") + .takes_value(true) + .requires("enable_permissioned_burn") + .help("Specify a permissioned burn authority for the mint. Defaults to the mint authority.") + ) .arg(multisig_signer_arg()) .nonce_args(true) .arg(memo_arg()) diff --git a/clients/cli/src/command.rs b/clients/cli/src/command.rs index 1bf75141b..e7890784d 100644 --- a/clients/cli/src/command.rs +++ b/clients/cli/src/command.rs @@ -52,6 +52,7 @@ use { mint_close_authority::MintCloseAuthority, pausable::PausableConfig, permanent_delegate::PermanentDelegate, + permissioned_burn::PermissionedBurnConfig, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, transfer_hook::TransferHook, @@ -269,6 +270,7 @@ async fn command_create_token( ui_multiplier: Option, pausable: bool, enable_permissioned_burn: bool, + permissioned_burn_authority: Option, bulk_signers: Vec>, ) -> CommandResult { println_display( @@ -411,7 +413,9 @@ async fn command_create_token( } if enable_permissioned_burn { - extensions.push(ExtensionInitializationParams::PermissionedBurnConfig { authority }); + extensions.push(ExtensionInitializationParams::PermissionedBurnConfig { + authority: permissioned_burn_authority.unwrap_or(authority), + }); } let res = token @@ -1129,6 +1133,16 @@ async fn command_authorize( )) } } + CliAuthorityType::PermissionedBurn => { + if let Ok(extension) = mint.get_extension::() { + Ok(Option::::from(extension.authority)) + } else { + Err(format!( + "Mint `{}` does not support permissioned burn", + account + )) + } + } }?; Ok((account, previous_authority)) @@ -1172,7 +1186,8 @@ async fn command_authorize( | CliAuthorityType::Group | CliAuthorityType::GroupMemberPointer | CliAuthorityType::ScaledUiAmount - | CliAuthorityType::Pause => Err(format!( + | CliAuthorityType::Pause + | CliAuthorityType::PermissionedBurn => Err(format!( "Authority type `{auth_str}` not supported for SPL Token accounts", )), CliAuthorityType::Owner => { @@ -3780,6 +3795,12 @@ pub async fn process_command( }); let transfer_hook_program_id = pubkey_of_signer(arg_matches, "transfer_hook", &mut wallet_manager).unwrap(); + let permissioned_burn_authority = pubkey_of_signer( + arg_matches, + "permissioned_burn_authority", + &mut wallet_manager, + ) + .unwrap(); let confidential_transfer_auto_approve = arg_matches .value_of("enable_confidential_transfers") @@ -3810,6 +3831,7 @@ pub async fn process_command( ui_multiplier, arg_matches.is_present("enable_pause"), arg_matches.is_present("enable_permissioned_burn"), + permissioned_burn_authority, bulk_signers, ) .await diff --git a/clients/cli/tests/command.rs b/clients/cli/tests/command.rs index e5a2bfc4d..0e0059270 100644 --- a/clients/cli/tests/command.rs +++ b/clients/cli/tests/command.rs @@ -4515,6 +4515,7 @@ async fn permissioned_burn(test_validator: &TestValidator, payer: &Keypair) { test_config_with_default_signer(test_validator, payer, &spl_token_2022_interface::id()); let token = Keypair::new(); + let burn_authority = Keypair::new(); let token_keypair_file = NamedTempFile::new().unwrap(); write_keypair_file(&token, &token_keypair_file).unwrap(); let token_pubkey = token.pubkey(); @@ -4527,6 +4528,8 @@ async fn permissioned_burn(test_validator: &TestValidator, payer: &Keypair) { CommandName::CreateToken.into(), token_keypair_file.path().to_str().unwrap(), "--enable-permissioned-burn", + "--permissioned-burn-authority", + burn_authority.pubkey().to_string().as_str(), ], ) .await @@ -4537,6 +4540,6 @@ async fn permissioned_burn(test_validator: &TestValidator, payer: &Keypair) { let extension = test_mint.get_extension::().unwrap(); assert_eq!( Option::::from(extension.authority), - Some(payer.pubkey()) + Some(burn_authority.pubkey()) ); } diff --git a/clients/rust-legacy/tests/permissioned_burn.rs b/clients/rust-legacy/tests/permissioned_burn.rs index 4c2644cd2..c089ab8ec 100644 --- a/clients/rust-legacy/tests/permissioned_burn.rs +++ b/clients/rust-legacy/tests/permissioned_burn.rs @@ -8,13 +8,13 @@ use { }, spl_token_2022_interface::{ error::TokenError, - instruction::AuthorityType, extension::{ permissioned_burn::{ instruction as permissioned_burn_instruction, PermissionedBurnConfig, }, BaseStateWithExtensions, }, + instruction::AuthorityType, }, spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, }; diff --git a/program/src/processor.rs b/program/src/processor.rs index 23db85087..a76939d34 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1101,17 +1101,14 @@ impl Processor { let source_account_info = next_account_info(account_info_iter)?; let mint_info = next_account_info(account_info_iter)?; - let (permissioned_burn_authority_info, authority_info) = - match burn_variant { - BurnInstructionVariant::Permissioned => { - let permissioned_burn_authority_info = next_account_info(account_info_iter)?; - let authority_info = next_account_info(account_info_iter)?; - (Some(permissioned_burn_authority_info), authority_info) - } - BurnInstructionVariant::Standard => { - (None, next_account_info(account_info_iter)?) - } - }; + let (permissioned_burn_authority_info, authority_info) = match burn_variant { + BurnInstructionVariant::Permissioned => { + let permissioned_burn_authority_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + (Some(permissioned_burn_authority_info), authority_info) + } + BurnInstructionVariant::Standard => (None, next_account_info(account_info_iter)?), + }; let authority_info_data_len = authority_info.data_len(); @@ -1132,8 +1129,8 @@ impl Processor { let ext = permissioned_ext.map_err(|_| TokenError::InvalidInstruction)?; // Pull the required extra signer from the accounts - let approver_ai = permissioned_burn_authority_info - .ok_or(ProgramError::NotEnoughAccountKeys)?; + let approver_ai = + permissioned_burn_authority_info.ok_or(ProgramError::NotEnoughAccountKeys)?; if !approver_ai.is_signer { return Err(ProgramError::MissingRequiredSignature); From ccc02386047628f0d59dee91f83e4ca375d85431 Mon Sep 17 00:00:00 2001 From: Sergej Date: Wed, 10 Dec 2025 09:52:36 +0100 Subject: [PATCH 25/32] handle None authority --- .../js/src/generated/instructions/index.ts | 2 +- clients/js/src/generated/types/extension.ts | 11 +++++----- .../permissioned_burn/instruction.rs | 6 +++-- .../extension/permissioned_burn/processor.rs | 2 +- program/src/processor.rs | 22 ++++++++++++++----- scripts/solana.dic | 1 + 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index 5c7da2372..da4eb8c23 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -56,7 +56,6 @@ export * from './initializeMultisig'; export * from './initializeMultisig2'; export * from './initializeNonTransferableMint'; export * from './initializePausableConfig'; -export * from './initializePermissionedBurn'; export * from './initializePermanentDelegate'; export * from './initializeScaledUiAmountMint'; export * from './initializeTokenGroup'; @@ -64,6 +63,7 @@ export * from './initializeTokenGroupMember'; export * from './initializeTokenMetadata'; export * from './initializeTransferFeeConfig'; export * from './initializeTransferHook'; +export * from './initializePermissionedBurn'; export * from './mintTo'; export * from './mintToChecked'; export * from './pause'; diff --git a/clients/js/src/generated/types/extension.ts b/clients/js/src/generated/types/extension.ts index df4e59d3a..68d39e79e 100644 --- a/clients/js/src/generated/types/extension.ts +++ b/clients/js/src/generated/types/extension.ts @@ -494,7 +494,10 @@ export type ExtensionArgs = paused: boolean; } | { __kind: 'PausableAccount' } - | { __kind: 'PermissionedBurn'; authority: OptionOrNullable
}; + | { + __kind: 'PermissionedBurn'; + authority: OptionOrNullable
; + }; export function getExtensionEncoder(): Encoder { return getDiscriminatedUnionEncoder( @@ -1429,11 +1432,7 @@ export function extension( '__kind', 'PermissionedBurn' > -): GetDiscriminatedUnionVariant< - ExtensionArgs, - '__kind', - 'PermissionedBurn' ->; +): GetDiscriminatedUnionVariant; export function extension( kind: K, data?: Data diff --git a/interface/src/extension/permissioned_burn/instruction.rs b/interface/src/extension/permissioned_burn/instruction.rs index 6f810947c..c9475d54b 100644 --- a/interface/src/extension/permissioned_burn/instruction.rs +++ b/interface/src/extension/permissioned_burn/instruction.rs @@ -35,13 +35,15 @@ pub enum PermissionedBurnInstruction { /// * Single authority /// 0. `[writable]` The source account to burn from. /// 1. `[writable]` The token mint. - /// 2. `[signer]` The permissioned burn authority configured on the mint. + /// 2. `[signer]` The permissioned burn authority configured on the mint, + /// if any. /// 3. `[signer]` The source account's owner/delegate. /// /// * Multisignature authority /// 0. `[writable]` The source account to burn from. /// 1. `[writable]` The token mint. - /// 2. `[signer]` The permissioned burn authority configured on the mint. + /// 2. `[signer]` The permissioned burn authority configured on the mint, + /// if any. /// 3. `[]` The source account's multisignature owner/delegate. /// 4. `..4+M` `[signer]` M signer accounts for the multisig. /// diff --git a/program/src/extension/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs index df3cabba9..e2b8f38ca 100644 --- a/program/src/extension/permissioned_burn/processor.rs +++ b/program/src/extension/permissioned_burn/processor.rs @@ -48,7 +48,7 @@ pub(crate) fn process_instruction( PermissionedBurnInstruction::Initialize => { msg!("PermissionedBurnInstruction::Initialize"); let InitializeInstructionData { authority } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, authority) + process_initialize(program_id, accounts, &authority) } PermissionedBurnInstruction::Burn => { msg!("PermissionedBurnInstruction::Burn"); diff --git a/program/src/processor.rs b/program/src/processor.rs index a76939d34..6cfab2ac3 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1116,28 +1116,38 @@ impl Processor { let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; let permissioned_ext = mint.get_extension::(); + let maybe_permissioned_burn_authority = + permissioned_ext + .as_ref() + .ok() + .and_then(|ext| Option::::from(ext.authority)); match burn_variant { BurnInstructionVariant::Standard => { // Standard burns cannot be used when the permissioned burn // extension is present. - if permissioned_ext.is_ok() { + if maybe_permissioned_burn_authority.is_some() { return Err(TokenError::InvalidInstruction.into()); } } BurnInstructionVariant::Permissioned => { - let ext = permissioned_ext.map_err(|_| TokenError::InvalidInstruction)?; + permissioned_ext.map_err(|_| TokenError::InvalidInstruction)?; + + let expected_burn_authority = maybe_permissioned_burn_authority + .ok_or_else(|| { + msg!("Permissioned burn authority is None; use the standard burn"); + TokenError::InvalidInstruction + })?; // Pull the required extra signer from the accounts - let approver_ai = - permissioned_burn_authority_info.ok_or(ProgramError::NotEnoughAccountKeys)?; + let approver_ai = permissioned_burn_authority_info + .ok_or(ProgramError::NotEnoughAccountKeys)?; 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 { + if *approver_ai.key != expected_burn_authority { return Err(ProgramError::InvalidAccountData); } } diff --git a/scripts/solana.dic b/scripts/solana.dic index df7bf086b..06948e019 100644 --- a/scripts/solana.dic +++ b/scripts/solana.dic @@ -69,3 +69,4 @@ cryptographic cryptographically prover encryptions +permissioned From a6e910be2184899b406aeed9b5da00b8cb1d9f66 Mon Sep 17 00:00:00 2001 From: Sergej Date: Wed, 10 Dec 2025 10:23:47 +0100 Subject: [PATCH 26/32] test None --- program/src/processor.rs | 132 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/program/src/processor.rs b/program/src/processor.rs index 6cfab2ac3..1e1faeea5 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -2129,7 +2129,13 @@ mod tests { solana_program_option::COption, solana_sdk_ids::sysvar::rent, spl_token_2022_interface::{ - extension::transfer_fee::instruction::initialize_transfer_fee_config, instruction::*, + extension::{ + permissioned_burn, + transfer_fee::instruction::initialize_transfer_fee_config, + ExtensionType, + }, + instruction::*, + pod::PodMint, state::Multisig, }, std::sync::{Arc, RwLock}, @@ -6188,6 +6194,130 @@ mod tests { .unwrap(); } + #[test] + fn test_permissioned_burn_none_authority_errors() { + let program_id = crate::id(); + let mint_key = Pubkey::new_unique(); + let owner_key = Pubkey::new_unique(); + let burn_authority_key = Pubkey::new_unique(); + let account_key = Pubkey::new_unique(); + + let mint_size = ExtensionType::try_calculate_account_len::(&[ + ExtensionType::PermissionedBurn, + ]) + .unwrap(); + + let mut mint_account = SolanaAccount::new( + Rent::default().minimum_balance(mint_size), + mint_size, + &program_id, + ); + let mut account_account = SolanaAccount::new( + account_minimum_balance(), + Account::get_packed_len(), + &program_id, + ); + let mut owner_account = SolanaAccount::default(); + let mut burn_authority_account = SolanaAccount::default(); + let mut rent_sysvar = rent_sysvar(); + + do_process_instruction( + permissioned_burn::instruction::initialize( + &program_id, + &mint_key, + &burn_authority_key, + ) + .unwrap(), + vec![&mut mint_account], + ) + .unwrap(); + do_process_instruction( + initialize_mint(&program_id, &mint_key, &owner_key, None, 2).unwrap(), + vec![&mut mint_account, &mut rent_sysvar], + ) + .unwrap(); + + // Create account and mint some tokens. + do_process_instruction( + initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), + vec![ + &mut account_account, + &mut mint_account, + &mut owner_account, + &mut rent_sysvar, + ], + ) + .unwrap(); + do_process_instruction( + mint_to( + &program_id, + &mint_key, + &account_key, + &owner_key, + &[], + 10, + ) + .unwrap(), + vec![&mut mint_account, &mut account_account, &mut owner_account], + ) + .unwrap(); + + // Clear the permissioned burn authority. + do_process_instruction( + set_authority( + &program_id, + &mint_key, + None, + AuthorityType::PermissionedBurn, + &burn_authority_key, + &[], + ) + .unwrap(), + vec![&mut mint_account, &mut burn_authority_account], + ) + .unwrap(); + + // Attempt a permissioned burn should fail when authority is None. + assert_eq!( + Err(TokenError::InvalidInstruction.into()), + do_process_instruction( + permissioned_burn::instruction::burn( + &program_id, + &account_key, + &mint_key, + &burn_authority_key, + &owner_key, + &[], + 1 + ) + .unwrap(), + vec![ + &mut account_account, + &mut mint_account, + &mut burn_authority_account, + &mut owner_account + ], + ) + ); + + // Standard burn should still succeed after authority is cleared. + assert_eq!( + Ok(()), + do_process_instruction( + burn( + &program_id, + &account_key, + &mint_key, + &owner_key, + &[], + 1 + ) + .unwrap(), + vec![&mut account_account, &mut mint_account, &mut owner_account], + ) + ); + } + #[test] fn test_validate_owner() { let program_id = crate::id(); From 7970ee9bf7a17d4526b3ad8758d2ae7a4c419538 Mon Sep 17 00:00:00 2001 From: Sergej Date: Wed, 10 Dec 2025 10:49:09 +0100 Subject: [PATCH 27/32] format & make clippy happy --- .../rust-legacy/tests/permissioned_burn.rs | 12 ++-- .../permissioned_burn/instruction.rs | 1 + .../extension/permissioned_burn/processor.rs | 2 +- program/src/processor.rs | 55 ++++++------------- 4 files changed, 24 insertions(+), 46 deletions(-) diff --git a/clients/rust-legacy/tests/permissioned_burn.rs b/clients/rust-legacy/tests/permissioned_burn.rs index c089ab8ec..c725fe7c7 100644 --- a/clients/rust-legacy/tests/permissioned_burn.rs +++ b/clients/rust-legacy/tests/permissioned_burn.rs @@ -94,7 +94,7 @@ async fn permissioned_burn_enforced() { let ix_wrong = permissioned_burn_instruction::burn_checked( &spl_token_2022_interface::id(), &account, - &token.get_address(), + token.get_address(), &wrong_permissioned.pubkey(), &account_owner.pubkey(), &[], @@ -117,7 +117,7 @@ async fn permissioned_burn_enforced() { let ix_ok = permissioned_burn_instruction::burn_checked( &spl_token_2022_interface::id(), &account, - &token.get_address(), + token.get_address(), &authority.pubkey(), &account_owner.pubkey(), &[], @@ -131,9 +131,9 @@ async fn permissioned_burn_enforced() { .unwrap(); let account_after = token.get_account_info(&account).await.unwrap(); - assert_eq!(u64::from(account_after.base.amount), 1); + assert_eq!(account_after.base.amount, 1); let mint_after = token.get_mint_info().await.unwrap(); - assert_eq!(u64::from(mint_after.base.supply), 1); + assert_eq!(mint_after.base.supply, 1); // Update permissioned burn authority and ensure new authority is enforced. token @@ -151,7 +151,7 @@ async fn permissioned_burn_enforced() { let ix_old = permissioned_burn_instruction::burn_checked( &spl_token_2022_interface::id(), &account, - &token.get_address(), + token.get_address(), &authority.pubkey(), &account_owner.pubkey(), &[], @@ -174,7 +174,7 @@ async fn permissioned_burn_enforced() { let ix_new = permissioned_burn_instruction::burn_checked( &spl_token_2022_interface::id(), &account, - &token.get_address(), + token.get_address(), &new_authority.pubkey(), &account_owner.pubkey(), &[], diff --git a/interface/src/extension/permissioned_burn/instruction.rs b/interface/src/extension/permissioned_burn/instruction.rs index c9475d54b..16324e147 100644 --- a/interface/src/extension/permissioned_burn/instruction.rs +++ b/interface/src/extension/permissioned_burn/instruction.rs @@ -151,6 +151,7 @@ pub fn burn( } /// Create a `BurnChecked` instruction using the permissioned burn extension. +#[allow(clippy::too_many_arguments)] pub fn burn_checked( token_program_id: &Pubkey, account: &Pubkey, diff --git a/program/src/extension/permissioned_burn/processor.rs b/program/src/extension/permissioned_burn/processor.rs index e2b8f38ca..df3cabba9 100644 --- a/program/src/extension/permissioned_burn/processor.rs +++ b/program/src/extension/permissioned_burn/processor.rs @@ -48,7 +48,7 @@ pub(crate) fn process_instruction( PermissionedBurnInstruction::Initialize => { msg!("PermissionedBurnInstruction::Initialize"); let InitializeInstructionData { authority } = decode_instruction_data(input)?; - process_initialize(program_id, accounts, &authority) + process_initialize(program_id, accounts, authority) } PermissionedBurnInstruction::Burn => { msg!("PermissionedBurnInstruction::Burn"); diff --git a/program/src/processor.rs b/program/src/processor.rs index 1e1faeea5..ae0834a6a 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -1116,11 +1116,10 @@ impl Processor { let mint = PodStateWithExtensionsMut::::unpack(&mut mint_data)?; let permissioned_ext = mint.get_extension::(); - let maybe_permissioned_burn_authority = - permissioned_ext - .as_ref() - .ok() - .and_then(|ext| Option::::from(ext.authority)); + let maybe_permissioned_burn_authority = permissioned_ext + .as_ref() + .ok() + .and_then(|ext| Option::::from(ext.authority)); match burn_variant { BurnInstructionVariant::Standard => { @@ -1133,15 +1132,15 @@ impl Processor { BurnInstructionVariant::Permissioned => { permissioned_ext.map_err(|_| TokenError::InvalidInstruction)?; - let expected_burn_authority = maybe_permissioned_burn_authority - .ok_or_else(|| { + let expected_burn_authority = + maybe_permissioned_burn_authority.ok_or_else(|| { msg!("Permissioned burn authority is None; use the standard burn"); TokenError::InvalidInstruction })?; // Pull the required extra signer from the accounts - let approver_ai = permissioned_burn_authority_info - .ok_or(ProgramError::NotEnoughAccountKeys)?; + let approver_ai = + permissioned_burn_authority_info.ok_or(ProgramError::NotEnoughAccountKeys)?; if !approver_ai.is_signer { return Err(ProgramError::MissingRequiredSignature); @@ -2130,8 +2129,7 @@ mod tests { solana_sdk_ids::sysvar::rent, spl_token_2022_interface::{ extension::{ - permissioned_burn, - transfer_fee::instruction::initialize_transfer_fee_config, + permissioned_burn, transfer_fee::instruction::initialize_transfer_fee_config, ExtensionType, }, instruction::*, @@ -6202,10 +6200,9 @@ mod tests { let burn_authority_key = Pubkey::new_unique(); let account_key = Pubkey::new_unique(); - let mint_size = ExtensionType::try_calculate_account_len::(&[ - ExtensionType::PermissionedBurn, - ]) - .unwrap(); + let mint_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::PermissionedBurn]) + .unwrap(); let mut mint_account = SolanaAccount::new( Rent::default().minimum_balance(mint_size), @@ -6222,12 +6219,8 @@ mod tests { let mut rent_sysvar = rent_sysvar(); do_process_instruction( - permissioned_burn::instruction::initialize( - &program_id, - &mint_key, - &burn_authority_key, - ) - .unwrap(), + permissioned_burn::instruction::initialize(&program_id, &mint_key, &burn_authority_key) + .unwrap(), vec![&mut mint_account], ) .unwrap(); @@ -6249,15 +6242,7 @@ mod tests { ) .unwrap(); do_process_instruction( - mint_to( - &program_id, - &mint_key, - &account_key, - &owner_key, - &[], - 10, - ) - .unwrap(), + mint_to(&program_id, &mint_key, &account_key, &owner_key, &[], 10).unwrap(), vec![&mut mint_account, &mut account_account, &mut owner_account], ) .unwrap(); @@ -6304,15 +6289,7 @@ mod tests { assert_eq!( Ok(()), do_process_instruction( - burn( - &program_id, - &account_key, - &mint_key, - &owner_key, - &[], - 1 - ) - .unwrap(), + burn(&program_id, &account_key, &mint_key, &owner_key, &[], 1).unwrap(), vec![&mut account_account, &mut mint_account, &mut owner_account], ) ); From 81b5af76276e438577b13404053cb1e3ff0a9e62 Mon Sep 17 00:00:00 2001 From: Sergej Date: Wed, 10 Dec 2025 11:05:13 +0100 Subject: [PATCH 28/32] generate clients --- .../js/src/generated/instructions/index.ts | 2 +- .../initializePermissionedBurn.ts | 5 +- .../instructions/permissionedBurn.ts | 54 +-- .../instructions/permissionedBurnChecked.ts | 50 +-- .../js/src/generated/programs/token2022.ts | 35 +- clients/js/src/generated/types/extension.ts | 47 +-- .../js/src/generated/types/extensionType.ts | 1 - interface/idl.json | 330 ++++++++++++++++++ 8 files changed, 402 insertions(+), 122 deletions(-) diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index da4eb8c23..9b417f6ec 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -57,13 +57,13 @@ export * from './initializeMultisig2'; export * from './initializeNonTransferableMint'; export * from './initializePausableConfig'; export * from './initializePermanentDelegate'; +export * from './initializePermissionedBurn'; export * from './initializeScaledUiAmountMint'; export * from './initializeTokenGroup'; export * from './initializeTokenGroupMember'; export * from './initializeTokenMetadata'; export * from './initializeTransferFeeConfig'; export * from './initializeTransferHook'; -export * from './initializePermissionedBurn'; export * from './mintTo'; export * from './mintToChecked'; export * from './pause'; diff --git a/clients/js/src/generated/instructions/initializePermissionedBurn.ts b/clients/js/src/generated/instructions/initializePermissionedBurn.ts index e5f1b336f..e1de968dd 100644 --- a/clients/js/src/generated/instructions/initializePermissionedBurn.ts +++ b/clients/js/src/generated/instructions/initializePermissionedBurn.ts @@ -107,9 +107,8 @@ export function getInitializePermissionedBurnInstructionDataCodec(): FixedSizeCo export type InitializePermissionedBurnInput< TAccountMint extends string = string, > = { - /** The mint to initialize. */ + /** The mint account to initialize. */ mint: Address; - /** The public key for the account that is required for token burning. */ authority: InitializePermissionedBurnInstructionDataArgs['authority']; }; @@ -151,7 +150,7 @@ export type ParsedInitializePermissionedBurnInstruction< > = { programAddress: Address; accounts: { - /** The mint to initialize. */ + /** The mint account to initialize. */ mint: TAccountMetas[0]; }; data: InitializePermissionedBurnInstructionData; diff --git a/clients/js/src/generated/instructions/permissionedBurn.ts b/clients/js/src/generated/instructions/permissionedBurn.ts index 7e441147e..39a35071b 100644 --- a/clients/js/src/generated/instructions/permissionedBurn.ts +++ b/clients/js/src/generated/instructions/permissionedBurn.ts @@ -52,10 +52,10 @@ export type PermissionedBurnInstruction< TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, TAccountAccount extends string | AccountMeta = string, TAccountMint extends string | AccountMeta = string, - TAccountAuthority extends string | AccountMeta = string, TAccountPermissionedBurnAuthority extends | string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, TRemainingAccounts extends readonly AccountMeta[] = [], > = Instruction & InstructionWithData & @@ -67,13 +67,13 @@ export type PermissionedBurnInstruction< TAccountMint extends string ? WritableAccount : TAccountMint, - TAccountAuthority extends string - ? ReadonlyAccount - : TAccountAuthority, TAccountPermissionedBurnAuthority extends string ? ReadonlySignerAccount & AccountSignerMeta : TAccountPermissionedBurnAuthority, + TAccountAuthority extends string + ? ReadonlyAccount + : TAccountAuthority, ...TRemainingAccounts, ] >; @@ -127,24 +127,17 @@ export function getPermissionedBurnInstructionDataCodec(): FixedSizeCodec< export type PermissionedBurnInput< TAccountAccount extends string = string, TAccountMint extends string = string, - TAccountAuthority extends string = string, TAccountPermissionedBurnAuthority extends string = string, + TAccountAuthority extends string = string, > = { /** The source account to burn from. */ account: Address; /** The token mint. */ mint: Address; + /** Authority configured on the mint that must sign any permissioned burn instruction. */ + permissionedBurnAuthority: TransactionSigner; /** The account's owner/delegate or its multisignature account. */ - authority: - | Address - | TransactionSigner; - /** - * Authority configured on the mint that must sign any permissioned burn - * instruction. - */ - permissionedBurnAuthority: - | Address - | TransactionSigner; + authority: Address | TransactionSigner; amount: PermissionedBurnInstructionDataArgs['amount']; multiSigners?: Array; }; @@ -152,29 +145,26 @@ export type PermissionedBurnInput< export function getPermissionedBurnInstruction< TAccountAccount extends string, TAccountMint extends string, - TAccountAuthority extends string, TAccountPermissionedBurnAuthority extends string, + TAccountAuthority extends string, TProgramAddress extends Address = typeof TOKEN_2022_PROGRAM_ADDRESS, >( input: PermissionedBurnInput< TAccountAccount, TAccountMint, - TAccountAuthority, - TAccountPermissionedBurnAuthority + TAccountPermissionedBurnAuthority, + TAccountAuthority >, config?: { programAddress?: TProgramAddress } ): PermissionedBurnInstruction< TProgramAddress, TAccountAccount, TAccountMint, + TAccountPermissionedBurnAuthority, (typeof input)['authority'] extends TransactionSigner ? ReadonlySignerAccount & AccountSignerMeta - : TAccountAuthority, - (typeof input)['permissionedBurnAuthority'] extends TransactionSigner - ? ReadonlySignerAccount & - AccountSignerMeta - : TAccountPermissionedBurnAuthority + : TAccountAuthority > { // Program address. const programAddress = config?.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS; @@ -223,14 +213,11 @@ export function getPermissionedBurnInstruction< TProgramAddress, TAccountAccount, TAccountMint, + TAccountPermissionedBurnAuthority, (typeof input)['authority'] extends TransactionSigner ? ReadonlySignerAccount & AccountSignerMeta - : TAccountAuthority, - (typeof input)['permissionedBurnAuthority'] extends TransactionSigner - ? ReadonlySignerAccount & - AccountSignerMeta - : TAccountPermissionedBurnAuthority + : TAccountAuthority >); } @@ -244,14 +231,10 @@ export type ParsedPermissionedBurnInstruction< account: TAccountMetas[0]; /** The token mint. */ mint: TAccountMetas[1]; - /** - * Authority configured on the mint that must sign any permissioned burn - * instruction. - */ + /** Authority configured on the mint that must sign any permissioned burn instruction. */ permissionedBurnAuthority: TAccountMetas[2]; /** The account's owner/delegate or its multisignature account. */ authority: TAccountMetas[3]; - multiSigners: TAccountMetas[4][]; }; data: PermissionedBurnInstructionData; }; @@ -281,10 +264,7 @@ export function parsePermissionedBurnInstruction< mint: getNextAccount(), permissionedBurnAuthority: getNextAccount(), authority: getNextAccount(), - multiSigners: instruction.accounts.slice(4) as TAccountMetas[4][], }, - data: getPermissionedBurnInstructionDataDecoder().decode( - instruction.data - ), + data: getPermissionedBurnInstructionDataDecoder().decode(instruction.data), }; } diff --git a/clients/js/src/generated/instructions/permissionedBurnChecked.ts b/clients/js/src/generated/instructions/permissionedBurnChecked.ts index 3c96ba047..ef6418487 100644 --- a/clients/js/src/generated/instructions/permissionedBurnChecked.ts +++ b/clients/js/src/generated/instructions/permissionedBurnChecked.ts @@ -52,10 +52,10 @@ export type PermissionedBurnCheckedInstruction< TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, TAccountAccount extends string | AccountMeta = string, TAccountMint extends string | AccountMeta = string, - TAccountAuthority extends string | AccountMeta = string, TAccountPermissionedBurnAuthority extends | string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, TRemainingAccounts extends readonly AccountMeta[] = [], > = Instruction & InstructionWithData & @@ -67,13 +67,13 @@ export type PermissionedBurnCheckedInstruction< TAccountMint extends string ? WritableAccount : TAccountMint, - TAccountAuthority extends string - ? ReadonlyAccount - : TAccountAuthority, TAccountPermissionedBurnAuthority extends string ? ReadonlySignerAccount & AccountSignerMeta : TAccountPermissionedBurnAuthority, + TAccountAuthority extends string + ? ReadonlyAccount + : TAccountAuthority, ...TRemainingAccounts, ] >; @@ -133,24 +133,17 @@ export function getPermissionedBurnCheckedInstructionDataCodec(): FixedSizeCodec export type PermissionedBurnCheckedInput< TAccountAccount extends string = string, TAccountMint extends string = string, - TAccountAuthority extends string = string, TAccountPermissionedBurnAuthority extends string = string, + TAccountAuthority extends string = string, > = { /** The source account to burn from. */ account: Address; /** The token mint. */ mint: Address; + /** Authority configured on the mint that must sign any permissioned burn instruction. */ + permissionedBurnAuthority: TransactionSigner; /** The account's owner/delegate or its multisignature account. */ - authority: - | Address - | TransactionSigner; - /** - * Authority configured on the mint that must sign any permissioned burn - * instruction. - */ - permissionedBurnAuthority: - | Address - | TransactionSigner; + authority: Address | TransactionSigner; amount: PermissionedBurnCheckedInstructionDataArgs['amount']; decimals: PermissionedBurnCheckedInstructionDataArgs['decimals']; multiSigners?: Array; @@ -159,29 +152,26 @@ export type PermissionedBurnCheckedInput< export function getPermissionedBurnCheckedInstruction< TAccountAccount extends string, TAccountMint extends string, - TAccountAuthority extends string, TAccountPermissionedBurnAuthority extends string, + TAccountAuthority extends string, TProgramAddress extends Address = typeof TOKEN_2022_PROGRAM_ADDRESS, >( input: PermissionedBurnCheckedInput< TAccountAccount, TAccountMint, - TAccountAuthority, - TAccountPermissionedBurnAuthority + TAccountPermissionedBurnAuthority, + TAccountAuthority >, config?: { programAddress?: TProgramAddress } ): PermissionedBurnCheckedInstruction< TProgramAddress, TAccountAccount, TAccountMint, + TAccountPermissionedBurnAuthority, (typeof input)['authority'] extends TransactionSigner ? ReadonlySignerAccount & AccountSignerMeta - : TAccountAuthority, - (typeof input)['permissionedBurnAuthority'] extends TransactionSigner - ? ReadonlySignerAccount & - AccountSignerMeta - : TAccountPermissionedBurnAuthority + : TAccountAuthority > { // Program address. const programAddress = config?.programAddress ?? TOKEN_2022_PROGRAM_ADDRESS; @@ -230,14 +220,11 @@ export function getPermissionedBurnCheckedInstruction< TProgramAddress, TAccountAccount, TAccountMint, + TAccountPermissionedBurnAuthority, (typeof input)['authority'] extends TransactionSigner ? ReadonlySignerAccount & AccountSignerMeta - : TAccountAuthority, - (typeof input)['permissionedBurnAuthority'] extends TransactionSigner - ? ReadonlySignerAccount & - AccountSignerMeta - : TAccountPermissionedBurnAuthority + : TAccountAuthority >); } @@ -251,14 +238,10 @@ export type ParsedPermissionedBurnCheckedInstruction< account: TAccountMetas[0]; /** The token mint. */ mint: TAccountMetas[1]; - /** - * Authority configured on the mint that must sign any permissioned burn - * instruction. - */ + /** Authority configured on the mint that must sign any permissioned burn instruction. */ permissionedBurnAuthority: TAccountMetas[2]; /** The account's owner/delegate or its multisignature account. */ authority: TAccountMetas[3]; - multiSigners: TAccountMetas[4][]; }; data: PermissionedBurnCheckedInstructionData; }; @@ -288,7 +271,6 @@ export function parsePermissionedBurnCheckedInstruction< mint: getNextAccount(), permissionedBurnAuthority: getNextAccount(), authority: getNextAccount(), - multiSigners: instruction.accounts.slice(4) as TAccountMetas[4][], }, data: getPermissionedBurnCheckedInstructionDataDecoder().decode( instruction.data diff --git a/clients/js/src/generated/programs/token2022.ts b/clients/js/src/generated/programs/token2022.ts index 510b3b398..c4e622d70 100644 --- a/clients/js/src/generated/programs/token2022.ts +++ b/clients/js/src/generated/programs/token2022.ts @@ -62,6 +62,7 @@ import { type ParsedInitializeNonTransferableMintInstruction, type ParsedInitializePausableConfigInstruction, type ParsedInitializePermanentDelegateInstruction, + type ParsedInitializePermissionedBurnInstruction, type ParsedInitializeScaledUiAmountMintInstruction, type ParsedInitializeTokenGroupInstruction, type ParsedInitializeTokenGroupMemberInstruction, @@ -71,6 +72,8 @@ import { type ParsedMintToCheckedInstruction, type ParsedMintToInstruction, type ParsedPauseInstruction, + type ParsedPermissionedBurnCheckedInstruction, + type ParsedPermissionedBurnInstruction, type ParsedReallocateInstruction, type ParsedRemoveTokenMetadataKeyInstruction, type ParsedResumeInstruction, @@ -217,6 +220,9 @@ export enum Token2022Instruction { UpdateTokenGroupMaxSize, UpdateTokenGroupUpdateAuthority, InitializeTokenGroupMember, + InitializePermissionedBurn, + PermissionedBurn, + PermissionedBurnChecked, } export function identifyToken2022Instruction( @@ -671,6 +677,24 @@ export function identifyToken2022Instruction( ) { return Token2022Instruction.InitializeTokenGroupMember; } + if ( + containsBytes(data, getU8Encoder().encode(46), 0) && + containsBytes(data, getU8Encoder().encode(0), 1) + ) { + return Token2022Instruction.InitializePermissionedBurn; + } + if ( + containsBytes(data, getU8Encoder().encode(46), 0) && + containsBytes(data, getU8Encoder().encode(1), 1) + ) { + return Token2022Instruction.PermissionedBurn; + } + if ( + containsBytes(data, getU8Encoder().encode(46), 0) && + containsBytes(data, getU8Encoder().encode(2), 1) + ) { + return Token2022Instruction.PermissionedBurnChecked; + } throw new Error( 'The provided instruction could not be identified as a token-2022 instruction.' ); @@ -939,4 +963,13 @@ export type ParsedToken2022Instruction< } & ParsedUpdateTokenGroupUpdateAuthorityInstruction) | ({ instructionType: Token2022Instruction.InitializeTokenGroupMember; - } & ParsedInitializeTokenGroupMemberInstruction); + } & ParsedInitializeTokenGroupMemberInstruction) + | ({ + instructionType: Token2022Instruction.InitializePermissionedBurn; + } & ParsedInitializePermissionedBurnInstruction) + | ({ + instructionType: Token2022Instruction.PermissionedBurn; + } & ParsedPermissionedBurnInstruction) + | ({ + instructionType: Token2022Instruction.PermissionedBurnChecked; + } & ParsedPermissionedBurnCheckedInstruction); diff --git a/clients/js/src/generated/types/extension.ts b/clients/js/src/generated/types/extension.ts index 68d39e79e..bcad0205e 100644 --- a/clients/js/src/generated/types/extension.ts +++ b/clients/js/src/generated/types/extension.ts @@ -275,8 +275,7 @@ export type Extension = newMultiplier: number; } | { __kind: 'PausableConfig'; authority: Option
; paused: boolean } - | { __kind: 'PausableAccount' } - | { __kind: 'PermissionedBurn'; authority: Option
}; + | { __kind: 'PausableAccount' }; export type ExtensionArgs = | { __kind: 'Uninitialized' } @@ -493,11 +492,7 @@ export type ExtensionArgs = authority: OptionOrNullable
; paused: boolean; } - | { __kind: 'PausableAccount' } - | { - __kind: 'PermissionedBurn'; - authority: OptionOrNullable
; - }; + | { __kind: 'PausableAccount' }; export function getExtensionEncoder(): Encoder { return getDiscriminatedUnionEncoder( @@ -821,21 +816,6 @@ export function getExtensionEncoder(): Encoder { ), ], ['PausableAccount', getUnitEncoder()], - [ - 'PermissionedBurn', - addEncoderSizePrefix( - getStructEncoder([ - [ - 'authority', - getOptionEncoder(getAddressEncoder(), { - prefix: null, - noneValue: 'zeroes', - }), - ], - ]), - getU16Encoder() - ), - ], ], { size: getU16Encoder() } ); @@ -1163,21 +1143,6 @@ export function getExtensionDecoder(): Decoder { ), ], ['PausableAccount', getUnitDecoder()], - [ - 'PermissionedBurn', - addDecoderSizePrefix( - getStructDecoder([ - [ - 'authority', - getOptionDecoder(getAddressDecoder(), { - prefix: null, - noneValue: 'zeroes', - }), - ], - ]), - getU16Decoder() - ), - ], ], { size: getU16Decoder() } ); @@ -1425,14 +1390,6 @@ export function extension( export function extension( kind: 'PausableAccount' ): GetDiscriminatedUnionVariant; -export function extension( - kind: 'PermissionedBurn', - data: GetDiscriminatedUnionVariantContent< - ExtensionArgs, - '__kind', - 'PermissionedBurn' - > -): GetDiscriminatedUnionVariant; export function extension( kind: K, data?: Data diff --git a/clients/js/src/generated/types/extensionType.ts b/clients/js/src/generated/types/extensionType.ts index 9e3ed7941..7b65d198c 100644 --- a/clients/js/src/generated/types/extensionType.ts +++ b/clients/js/src/generated/types/extensionType.ts @@ -50,7 +50,6 @@ export enum ExtensionType { TokenGroup, GroupMemberPointer, TokenGroupMember, - PermissionedBurn, } export type ExtensionTypeArgs = ExtensionType; diff --git a/interface/idl.json b/interface/idl.json index 1b5b08fb7..56c57ae71 100644 --- a/interface/idl.json +++ b/interface/idl.json @@ -8453,6 +8453,336 @@ "offset": 0 } ] + }, + { + "kind": "instructionNode", + "name": "initializePermissionedBurn", + "docs": [ + "Require permissioned burn for the given mint account.", + "", + "Fails if the mint has already been initialized, so must be called before `InitializeMint`." + ], + "optionalAccountStrategy": "programId", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "mint", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [ + "The mint account to initialize." + ] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 46 + } + }, + { + "kind": "instructionArgumentNode", + "name": "permissionedBurnDiscriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 0 + } + }, + { + "kind": "instructionArgumentNode", + "name": "authority", + "docs": [ + "The public key for the account that is required for token burning." + ], + "type": { + "kind": "publicKeyTypeNode" + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + }, + { + "kind": "fieldDiscriminatorNode", + "name": "permissionedBurnDiscriminator", + "offset": 1 + } + ] + }, + { + "kind": "instructionNode", + "name": "permissionedBurn", + "docs": [ + "Burn tokens when the mint has the permissioned burn extension enabled." + ], + "optionalAccountStrategy": "programId", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "account", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [ + "The source account to burn from." + ] + }, + { + "kind": "instructionAccountNode", + "name": "mint", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [ + "The token mint." + ] + }, + { + "kind": "instructionAccountNode", + "name": "permissionedBurnAuthority", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [ + "Authority configured on the mint that must sign any permissioned burn instruction." + ] + }, + { + "kind": "instructionAccountNode", + "name": "authority", + "isWritable": false, + "isSigner": "either", + "isOptional": false, + "docs": [ + "The account's owner/delegate or its multisignature account." + ], + "defaultValue": { + "kind": "identityValueNode" + } + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 46 + } + }, + { + "kind": "instructionArgumentNode", + "name": "permissionedBurnDiscriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 1 + } + }, + { + "kind": "instructionArgumentNode", + "name": "amount", + "docs": [ + "The amount of tokens to burn." + ], + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + } + } + ], + "remainingAccounts": [ + { + "kind": "instructionRemainingAccountsNode", + "isOptional": true, + "isSigner": true, + "docs": [], + "value": { + "kind": "argumentValueNode", + "name": "multiSigners" + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + }, + { + "kind": "fieldDiscriminatorNode", + "name": "permissionedBurnDiscriminator", + "offset": 1 + } + ] + }, + { + "kind": "instructionNode", + "name": "permissionedBurnChecked", + "docs": [ + "Burn tokens with expected decimals when the mint has the permissioned burn extension enabled." + ], + "optionalAccountStrategy": "programId", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "account", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [ + "The source account to burn from." + ] + }, + { + "kind": "instructionAccountNode", + "name": "mint", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": [ + "The token mint." + ] + }, + { + "kind": "instructionAccountNode", + "name": "permissionedBurnAuthority", + "isWritable": false, + "isSigner": true, + "isOptional": false, + "docs": [ + "Authority configured on the mint that must sign any permissioned burn instruction." + ] + }, + { + "kind": "instructionAccountNode", + "name": "authority", + "isWritable": false, + "isSigner": "either", + "isOptional": false, + "docs": [ + "The account's owner/delegate or its multisignature account." + ], + "defaultValue": { + "kind": "identityValueNode" + } + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 46 + } + }, + { + "kind": "instructionArgumentNode", + "name": "permissionedBurnDiscriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 2 + } + }, + { + "kind": "instructionArgumentNode", + "name": "amount", + "docs": [ + "The amount of tokens to burn." + ], + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + } + }, + { + "kind": "instructionArgumentNode", + "name": "decimals", + "docs": [ + "Expected number of base 10 digits to the right of the decimal place." + ], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + } + } + ], + "remainingAccounts": [ + { + "kind": "instructionRemainingAccountsNode", + "isOptional": true, + "isSigner": true, + "docs": [], + "value": { + "kind": "argumentValueNode", + "name": "multiSigners" + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + }, + { + "kind": "fieldDiscriminatorNode", + "name": "permissionedBurnDiscriminator", + "offset": 1 + } + ] } ], "definedTypes": [ From fa8911bb5523c4646e5773273270517a97707e0d Mon Sep 17 00:00:00 2001 From: Sergej Sakac <73715684+Szegoo@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:03:50 +0100 Subject: [PATCH 29/32] Update clients/cli/src/clap_app.rs Co-authored-by: Jon C --- clients/cli/src/clap_app.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clients/cli/src/clap_app.rs b/clients/cli/src/clap_app.rs index 919bc754a..212ed2b8e 100644 --- a/clients/cli/src/clap_app.rs +++ b/clients/cli/src/clap_app.rs @@ -935,12 +935,12 @@ pub fn app<'a>( .help("Require the configured permissioned burn authority for burning tokens") ) .arg( - Arg::with_name("permissioned_burn_authority") - .long("permissioned-burn-authority") - .validator(|s| is_valid_signer(s)) - .value_name("SIGNER") + Arg::with_name("permissioned_burn") + .long("permissioned-burn") + .validator(|s| is_valid_pubkey(s)) + .value_name("AUTHORITY") .takes_value(true) - .requires("enable_permissioned_burn") + .conflicts_with("enable_permissioned_burn") .help("Specify a permissioned burn authority for the mint. Defaults to the mint authority.") ) .arg(multisig_signer_arg()) From 789a2067f4457440c8dd6964fc9802872e36ce3c Mon Sep 17 00:00:00 2001 From: Sergej Date: Wed, 17 Dec 2025 17:42:58 +0100 Subject: [PATCH 30/32] fixes --- .../test/e2e-2022/permissionedBurn.test.ts | 22 +++++--- .../js/src/generated/programs/token2022.ts | 12 +++-- .../js/src/generated/types/authorityType.ts | 1 + clients/js/src/generated/types/extension.ts | 52 ++++++++++++++++++- interface/idl.json | 4 ++ interface/src/extension/mod.rs | 2 +- 6 files changed, 78 insertions(+), 15 deletions(-) diff --git a/clients/js-legacy/test/e2e-2022/permissionedBurn.test.ts b/clients/js-legacy/test/e2e-2022/permissionedBurn.test.ts index c6f3d1a24..0a0c07ca6 100644 --- a/clients/js-legacy/test/e2e-2022/permissionedBurn.test.ts +++ b/clients/js-legacy/test/e2e-2022/permissionedBurn.test.ts @@ -65,12 +65,20 @@ describe('permissioned burn', () => { it('enforces permissioned authority for burn', async () => { const owner = Keypair.generate(); - const account = await createAccount(connection, payer, mint, owner.publicKey, undefined, undefined, TEST_PROGRAM_ID); + const account = await createAccount( + connection, + payer, + mint, + owner.publicKey, + undefined, + undefined, + TEST_PROGRAM_ID, + ); await mintTo(connection, payer, mint, account, mintAuthority, 2, [], undefined, TEST_PROGRAM_ID); - await expect(burn(connection, payer, account, mint, owner, 1, [], undefined, TEST_PROGRAM_ID)).to.be.rejectedWith( - Error, - ); + await expect( + burn(connection, payer, account, mint, owner, 1, [], undefined, TEST_PROGRAM_ID), + ).to.be.rejectedWith(Error); const wrongPermissioned = Keypair.generate(); const badBurnTx = new Transaction().add( @@ -85,9 +93,9 @@ describe('permissioned burn', () => { TEST_PROGRAM_ID, ), ); - await expect(sendAndConfirmTransaction(connection, badBurnTx, [payer, owner, wrongPermissioned])).to.be.rejectedWith( - Error, - ); + await expect( + sendAndConfirmTransaction(connection, badBurnTx, [payer, owner, wrongPermissioned]), + ).to.be.rejectedWith(Error); const burnTx = new Transaction().add( createPermissionedBurnCheckedInstruction( diff --git a/clients/js/src/generated/programs/token2022.ts b/clients/js/src/generated/programs/token2022.ts index 7998c3fde..580592c97 100644 --- a/clients/js/src/generated/programs/token2022.ts +++ b/clients/js/src/generated/programs/token2022.ts @@ -221,10 +221,10 @@ export enum Token2022Instruction { UpdateTokenGroupMaxSize, UpdateTokenGroupUpdateAuthority, InitializeTokenGroupMember, + UnwrapLamports, InitializePermissionedBurn, PermissionedBurn, PermissionedBurnChecked, - UnwrapLamports, } export function identifyToken2022Instruction( @@ -679,6 +679,9 @@ export function identifyToken2022Instruction( ) { return Token2022Instruction.InitializeTokenGroupMember; } + if (containsBytes(data, getU8Encoder().encode(45), 0)) { + return Token2022Instruction.UnwrapLamports; + } if ( containsBytes(data, getU8Encoder().encode(46), 0) && containsBytes(data, getU8Encoder().encode(0), 1) @@ -696,8 +699,6 @@ export function identifyToken2022Instruction( containsBytes(data, getU8Encoder().encode(2), 1) ) { return Token2022Instruction.PermissionedBurnChecked; - if (containsBytes(data, getU8Encoder().encode(45), 0)) { - return Token2022Instruction.UnwrapLamports; } throw new Error( 'The provided instruction could not be identified as a token-2022 instruction.' @@ -968,6 +969,9 @@ export type ParsedToken2022Instruction< | ({ instructionType: Token2022Instruction.InitializeTokenGroupMember; } & ParsedInitializeTokenGroupMemberInstruction) + | ({ + instructionType: Token2022Instruction.UnwrapLamports; + } & ParsedUnwrapLamportsInstruction) | ({ instructionType: Token2022Instruction.InitializePermissionedBurn; } & ParsedInitializePermissionedBurnInstruction) @@ -977,5 +981,3 @@ export type ParsedToken2022Instruction< | ({ instructionType: Token2022Instruction.PermissionedBurnChecked; } & ParsedPermissionedBurnCheckedInstruction); - instructionType: Token2022Instruction.UnwrapLamports; - } & ParsedUnwrapLamportsInstruction); \ No newline at end of file diff --git a/clients/js/src/generated/types/authorityType.ts b/clients/js/src/generated/types/authorityType.ts index 6c7c8ae8b..9c3af5f61 100644 --- a/clients/js/src/generated/types/authorityType.ts +++ b/clients/js/src/generated/types/authorityType.ts @@ -33,6 +33,7 @@ export enum AuthorityType { GroupMemberPointer, ScaledUiAmount, Pause, + PermissionedBurn, } export type AuthorityTypeArgs = AuthorityType; diff --git a/clients/js/src/generated/types/extension.ts b/clients/js/src/generated/types/extension.ts index bcad0205e..c4cad72ac 100644 --- a/clients/js/src/generated/types/extension.ts +++ b/clients/js/src/generated/types/extension.ts @@ -275,7 +275,12 @@ export type Extension = newMultiplier: number; } | { __kind: 'PausableConfig'; authority: Option
; paused: boolean } - | { __kind: 'PausableAccount' }; + | { __kind: 'PausableAccount' } + | { + __kind: 'PermissionedBurn'; + /** Authority that is required for burning */ + authority: Option
; + }; export type ExtensionArgs = | { __kind: 'Uninitialized' } @@ -492,7 +497,12 @@ export type ExtensionArgs = authority: OptionOrNullable
; paused: boolean; } - | { __kind: 'PausableAccount' }; + | { __kind: 'PausableAccount' } + | { + __kind: 'PermissionedBurn'; + /** Authority that is required for burning */ + authority: OptionOrNullable
; + }; export function getExtensionEncoder(): Encoder { return getDiscriminatedUnionEncoder( @@ -816,6 +826,21 @@ export function getExtensionEncoder(): Encoder { ), ], ['PausableAccount', getUnitEncoder()], + [ + 'PermissionedBurn', + addEncoderSizePrefix( + getStructEncoder([ + [ + 'authority', + getOptionEncoder(getAddressEncoder(), { + prefix: null, + noneValue: 'zeroes', + }), + ], + ]), + getU16Encoder() + ), + ], ], { size: getU16Encoder() } ); @@ -1143,6 +1168,21 @@ export function getExtensionDecoder(): Decoder { ), ], ['PausableAccount', getUnitDecoder()], + [ + 'PermissionedBurn', + addDecoderSizePrefix( + getStructDecoder([ + [ + 'authority', + getOptionDecoder(getAddressDecoder(), { + prefix: null, + noneValue: 'zeroes', + }), + ], + ]), + getU16Decoder() + ), + ], ], { size: getU16Decoder() } ); @@ -1390,6 +1430,14 @@ export function extension( export function extension( kind: 'PausableAccount' ): GetDiscriminatedUnionVariant; +export function extension( + kind: 'PermissionedBurn', + data: GetDiscriminatedUnionVariantContent< + ExtensionArgs, + '__kind', + 'PermissionedBurn' + > +): GetDiscriminatedUnionVariant; export function extension( kind: K, data?: Data diff --git a/interface/idl.json b/interface/idl.json index 02db06e43..6adf23c45 100644 --- a/interface/idl.json +++ b/interface/idl.json @@ -8981,6 +8981,10 @@ { "kind": "enumEmptyVariantTypeNode", "name": "pause" + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "permissionedBurn" } ], "size": { diff --git a/interface/src/extension/mod.rs b/interface/src/extension/mod.rs index 4065e1446..d84c604b8 100644 --- a/interface/src/extension/mod.rs +++ b/interface/src/extension/mod.rs @@ -1122,7 +1122,7 @@ pub enum ExtensionType { Pausable, /// Indicates that the account belongs to a pausable mint PausableAccount, - /// Tokens burning requires approval from authorirty. + /// Tokens burning requires approval from authority. PermissionedBurn, /// Test variable-length mint extension From b36ebfc5ae823ba4706d76f5f1702076a600bf29 Mon Sep 17 00:00:00 2001 From: Sergej Date: Sun, 21 Dec 2025 14:28:43 +0100 Subject: [PATCH 31/32] fixes --- clients/cli/src/command.rs | 12 +++++------- clients/cli/tests/command.rs | 3 +-- interface/src/instruction.rs | 1 + program/src/processor.rs | 1 + 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/clients/cli/src/command.rs b/clients/cli/src/command.rs index e7a2aa944..d09ba6341 100644 --- a/clients/cli/src/command.rs +++ b/clients/cli/src/command.rs @@ -3892,12 +3892,10 @@ pub async fn process_command( }); let transfer_hook_program_id = pubkey_of_signer(arg_matches, "transfer_hook", &mut wallet_manager).unwrap(); - let permissioned_burn_authority = pubkey_of_signer( - arg_matches, - "permissioned_burn_authority", - &mut wallet_manager, - ) - .unwrap(); + let permissioned_burn_authority = + pubkey_of_signer(arg_matches, "permissioned_burn", &mut wallet_manager).unwrap(); + let enable_permissioned_burn = arg_matches.is_present("enable_permissioned_burn") + || permissioned_burn_authority.is_some(); let confidential_transfer_auto_approve = arg_matches .value_of("enable_confidential_transfers") @@ -3927,7 +3925,7 @@ pub async fn process_command( arg_matches.is_present("enable_transfer_hook"), ui_multiplier, arg_matches.is_present("enable_pause"), - arg_matches.is_present("enable_permissioned_burn"), + enable_permissioned_burn, permissioned_burn_authority, bulk_signers, ) diff --git a/clients/cli/tests/command.rs b/clients/cli/tests/command.rs index 895982c15..10b3a90aa 100644 --- a/clients/cli/tests/command.rs +++ b/clients/cli/tests/command.rs @@ -4826,8 +4826,7 @@ async fn permissioned_burn(test_validator: &TestValidator, payer: &Keypair) { "spl-token", CommandName::CreateToken.into(), token_keypair_file.path().to_str().unwrap(), - "--enable-permissioned-burn", - "--permissioned-burn-authority", + "--permissioned-burn", burn_authority.pubkey().to_string().as_str(), ], ) diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index 15840e250..4f2d4d598 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -1084,6 +1084,7 @@ impl<'a> TokenInstruction<'a> { } &Self::PermissionedBurnExtension => { buf.push(46); + } }; buf } diff --git a/program/src/processor.rs b/program/src/processor.rs index d56033d55..183f15677 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -2094,6 +2094,7 @@ impl Processor { accounts, &input[1..], ) + } PodTokenInstruction::UnwrapLamports => { msg!("Instruction: UnwrapLamports"); let (_, amount) = decode_instruction_data_with_coption_u64::<()>(input)?; From 96414d282e44c4ab9cb98ea476f78201df0d7a44 Mon Sep 17 00:00:00 2001 From: Sergej Date: Sat, 10 Jan 2026 12:59:35 +0100 Subject: [PATCH 32/32] fix nits --- interface/src/instruction.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs index 4f2d4d598..6039d5f61 100644 --- a/interface/src/instruction.rs +++ b/interface/src/instruction.rs @@ -731,8 +731,6 @@ pub enum TokenInstruction<'a> { ScaledUiAmountExtension, /// Instruction prefix for instructions to the pausable extension PausableExtension, - /// Instruction prefix for instructions to the permissioned burn extension - PermissionedBurnExtension, // 45 /// Transfer lamports from a native SOL account to a destination account. /// @@ -751,6 +749,8 @@ pub enum TokenInstruction<'a> { #[cfg_attr(feature = "serde", serde(with = "coption_u64_fromval"))] amount: COption, }, + /// Instruction prefix for instructions to the permissioned burn extension + PermissionedBurnExtension, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a @@ -893,11 +893,11 @@ impl<'a> TokenInstruction<'a> { 42 => Self::ConfidentialMintBurnExtension, 43 => Self::ScaledUiAmountExtension, 44 => Self::PausableExtension, - 46 => Self::PermissionedBurnExtension, 45 => { let (amount, _rest) = Self::unpack_u64_option(rest)?; Self::UnwrapLamports { amount } } + 46 => Self::PermissionedBurnExtension, _ => return Err(TokenError::InvalidInstruction.into()), }) }