Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion interface/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use {
non_transferable::{NonTransferable, NonTransferableAccount},
pausable::{PausableAccount, PausableConfig},
permanent_delegate::PermanentDelegate,
permissioned_burn::PermissionedBurnConfig,
scaled_ui_amount::ScaledUiAmountConfig,
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
transfer_hook::{TransferHook, TransferHookAccount},
Expand Down Expand Up @@ -76,6 +77,8 @@ pub mod non_transferable;
pub mod pausable;
/// Permanent Delegate extension
pub mod permanent_delegate;
/// Permissioned burn extension
pub mod permissioned_burn;
/// Scaled UI Amount extension
pub mod scaled_ui_amount;
/// Token-group extension
Expand Down Expand Up @@ -1119,6 +1122,8 @@ pub enum ExtensionType {
Pausable,
/// Indicates that the account belongs to a pausable mint
PausableAccount,
/// Tokens burning requires approval from authorirty.
PermissionedBurn,

/// Test variable-length mint extension
#[cfg(test)]
Expand Down Expand Up @@ -1204,6 +1209,7 @@ impl ExtensionType {
ExtensionType::ScaledUiAmount => pod_get_packed_len::<ScaledUiAmountConfig>(),
ExtensionType::Pausable => pod_get_packed_len::<PausableConfig>(),
ExtensionType::PausableAccount => pod_get_packed_len::<PausableAccount>(),
ExtensionType::PermissionedBurn => pod_get_packed_len::<PermissionedBurnConfig>(),
#[cfg(test)]
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
#[cfg(test)]
Expand Down Expand Up @@ -1270,7 +1276,8 @@ impl ExtensionType {
| ExtensionType::ConfidentialMintBurn
| ExtensionType::TokenGroupMember
| ExtensionType::ScaledUiAmount
| ExtensionType::Pausable => AccountType::Mint,
| ExtensionType::Pausable
| ExtensionType::PermissionedBurn => AccountType::Mint,
ExtensionType::ImmutableOwner
| ExtensionType::TransferFeeAmount
| ExtensionType::ConfidentialTransferAccount
Expand Down
59 changes: 59 additions & 0 deletions interface/src/extension/permissioned_burn/instruction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use {
crate::{
check_program_account,
instruction::{encode_instruction, TokenInstruction},
},
bytemuck::{Pod, Zeroable},
num_enum::{IntoPrimitive, TryFromPrimitive},
solana_instruction::{AccountMeta, Instruction},
solana_program_error::ProgramError,
solana_pubkey::Pubkey,
};

/// Permissioned Burn extension instructions
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum PermissionedBurnInstruction {
/// Require permissioned burn for the given mint account
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` The mint account to initialize.
///
/// Data expected by this instruction:
/// `crate::extension::permissioned_burn::instruction::InitializeInstructionData`
Initialize,
}

/// Data expected by `PermissionedBurnInstruction::Initialize`
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct InitializeInstructionData {
/// The public key for the account that is required for token burning.
pub authority: Pubkey,
}

/// Create an `Initialize` instruction
pub fn initialize(
token_program_id: &Pubkey,
mint: &Pubkey,
authority: &Pubkey,
) -> Result<Instruction, ProgramError> {
check_program_account(token_program_id)?;
let accounts = vec![AccountMeta::new(*mint, false)];
Ok(encode_instruction(
token_program_id,
accounts,
TokenInstruction::PermissionedBurnExtension,
PermissionedBurnInstruction::Initialize,
&InitializeInstructionData {
authority: *authority,
},
))
}
24 changes: 24 additions & 0 deletions interface/src/extension/permissioned_burn/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use {
crate::extension::{Extension, ExtensionType},
bytemuck::{Pod, Zeroable},
spl_pod::optional_keys::OptionalNonZeroPubkey
};

/// Instruction types for the permissioned burn extension
pub mod instruction;

/// Indicates that the tokens from this mint require permissioned burn
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
#[repr(C)]
pub struct PermissionedBurnConfig {
/// Authority that is required for burning
pub authority: OptionalNonZeroPubkey,
}

impl Extension for PermissionedBurnConfig {
const TYPE: ExtensionType = ExtensionType::PermissionedBurn;
}
10 changes: 10 additions & 0 deletions interface/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -873,6 +875,7 @@ impl<'a> TokenInstruction<'a> {
42 => Self::ConfidentialMintBurnExtension,
43 => Self::ScaledUiAmountExtension,
44 => Self::PausableExtension,
46 => Self::PermissionedBurnExtension,
_ => return Err(TokenError::InvalidInstruction.into()),
})
}
Expand Down Expand Up @@ -1053,6 +1056,9 @@ impl<'a> TokenInstruction<'a> {
&Self::PausableExtension => {
buf.push(44);
}
&Self::PermissionedBurnExtension => {
buf.push(46);
}
};
buf
}
Expand Down Expand Up @@ -1154,6 +1160,8 @@ pub enum AuthorityType {
ScaledUiAmount,
/// Authority to pause or resume minting / transferring / burning
Pause,
/// Authority to perform a permissioned token burn
PermissionedBurn,
}

impl AuthorityType {
Expand All @@ -1176,6 +1184,7 @@ impl AuthorityType {
AuthorityType::GroupMemberPointer => 14,
AuthorityType::ScaledUiAmount => 15,
AuthorityType::Pause => 16,
AuthorityType::PermissionedBurn => 17,
}
}

Expand All @@ -1199,6 +1208,7 @@ impl AuthorityType {
14 => Ok(AuthorityType::GroupMemberPointer),
15 => Ok(AuthorityType::ScaledUiAmount),
16 => Ok(AuthorityType::Pause),
17 => Ok(AuthorityType::PermissionedBurn),
_ => Err(TokenError::InvalidInstruction.into()),
}
}
Expand Down
2 changes: 2 additions & 0 deletions program/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pub mod non_transferable;
pub mod pausable;
/// Permanent Delegate extension
pub mod permanent_delegate;
/// Permissioned burn extension
pub mod permissioned_burn;
/// Utility to reallocate token accounts
pub mod reallocate;
/// Scaled UI Amount extension
Expand Down
5 changes: 5 additions & 0 deletions program/src/extension/permissioned_burn/instruction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![deprecated(
since = "9.1.0",
note = "Use spl_token_2022_interface instead and remove spl_token_2022 as a dependency"
)]
pub use spl_token_2022_interface::extension::permissioned_burn::instruction::*;
10 changes: 10 additions & 0 deletions program/src/extension/permissioned_burn/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// Instruction types for the permissioned burn extension
pub mod instruction;
/// Instruction processor for the permissioned burn extension
pub mod processor;

#[deprecated(
since = "9.1.0",
note = "Use spl_token_2022_interface instead and remove spl_token_2022 as a dependency"
)]
pub use spl_token_2022_interface::extension::permissioned_burn::PermissionedBurnConfig;
50 changes: 50 additions & 0 deletions program/src/extension/permissioned_burn/processor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use {
solana_account_info::{next_account_info, AccountInfo},
solana_msg::msg,
solana_program_error::ProgramResult,
solana_pubkey::Pubkey,
spl_token_2022_interface::{
check_program_account,
extension::{
permissioned_burn::{
instruction::{InitializeInstructionData, PermissionedBurnInstruction},
PermissionedBurnConfig,
},
BaseStateWithExtensionsMut, PodStateWithExtensionsMut,
},
instruction::{decode_instruction_data, decode_instruction_type},
pod::PodMint,
},
};

fn process_initialize(
_program_id: &Pubkey,
accounts: &[AccountInfo],
authority: &Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let mint_account_info = next_account_info(account_info_iter)?;
let mut mint_data = mint_account_info.data.borrow_mut();
let mut mint = PodStateWithExtensionsMut::<PodMint>::unpack_uninitialized(&mut mint_data)?;

let extension = mint.init_extension::<PermissionedBurnConfig>(true)?;
extension.authority = Some(*authority).try_into()?;

Ok(())
}

pub(crate) fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
check_program_account(program_id)?;

match decode_instruction_type(input)? {
PermissionedBurnInstruction::Initialize => {
msg!("PermissionedBurnInstruction::Initialize");
let InitializeInstructionData { authority } = decode_instruction_data(input)?;
process_initialize(program_id, accounts, authority)
}
}
}
1 change: 1 addition & 0 deletions program/src/pod_instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub(crate) enum PodTokenInstruction {
ConfidentialMintBurnExtension,
ScaledUiAmountExtension,
PausableExtension,
PermissionedBurnExtension,
}

fn unpack_pubkey_option(input: &[u8]) -> Result<PodCOption<Pubkey>, ProgramError> {
Expand Down
40 changes: 38 additions & 2 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use {
cpi_guard::{self, in_cpi},
default_account_state, group_member_pointer, group_pointer, interest_bearing_mint,
memo_transfer::{self, check_previous_sibling_instruction_is_memo},
metadata_pointer, pausable, reallocate, scaled_ui_amount, token_group, token_metadata,
transfer_fee, transfer_hook,
metadata_pointer, pausable, permissioned_burn, reallocate, scaled_ui_amount,
token_group, token_metadata, transfer_fee, transfer_hook,
},
pod_instruction::{
decode_instruction_data_with_coption_pubkey, AmountCheckedData, AmountData,
Expand Down Expand Up @@ -51,6 +51,7 @@ use {
non_transferable::{NonTransferable, NonTransferableAccount},
pausable::{PausableAccount, PausableConfig},
permanent_delegate::{get_permanent_delegate, PermanentDelegate},
permissioned_burn::PermissionedBurnConfig,
scaled_ui_amount::ScaledUiAmountConfig,
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
transfer_hook::{TransferHook, TransferHookAccount},
Expand Down Expand Up @@ -966,6 +967,19 @@ impl Processor {
)?;
extension.authority = new_authority.try_into()?;
}
AuthorityType::PermissionedBurn => {
let extension = mint.get_extension_mut::<PermissionedBurnConfig>()?;
let maybe_authority: Option<Pubkey> = 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());
}
Expand Down Expand Up @@ -1109,6 +1123,20 @@ impl Processor {
return Err(TokenError::MintPaused.into());
}
}
if let Ok(ext) = mint.get_extension::<PermissionedBurnConfig>() {
// Pull the required extra signer from the accounts
let approver_ai = next_account_info(account_info_iter)?;
Comment on lines +1127 to +1128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing this, I think we should add a new variant to Burn, like BurnCheckedWithPermission, which explicitly takes in the authority. Otherwise, the account order for multisig owners will break, since you'll need to do:

  • source account
  • mint account
  • account authority (non-signer)
  • permissioned burn authority (signer)
  • multisig signers (signer)

It would be better to be clear, and have a new instruction which orders the accounts as:

  • source
  • mint
  • permissioned burn authority
  • account authority
  • multisig signers

We can add it as another instruction under the PermissionedBurn extension.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense; definitely better than making a breaking change.


if !approver_ai.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}

let maybe_burn_authority: Option<Pubkey> = ext.authority.into();
if Some(*approver_ai.key) != maybe_burn_authority {
return Err(ProgramError::InvalidAccountData);
}
}

let maybe_permanent_delegate = get_permanent_delegate(&mint);

if let Ok(cpi_guard) = source_account.get_extension::<CpiGuard>() {
Expand Down Expand Up @@ -1942,6 +1970,14 @@ impl Processor {
msg!("Instruction: PausableExtension");
pausable::processor::process_instruction(program_id, accounts, &input[1..])
}
PodTokenInstruction::PermissionedBurnExtension => {
msg!("Instruction: PermissionedBurnExtension");
permissioned_burn::processor::process_instruction(
program_id,
accounts,
&input[1..],
)
}
}
} else if let Ok(instruction) = TokenMetadataInstruction::unpack(input) {
token_metadata::processor::process_instruction(program_id, accounts, instruction)
Expand Down