diff --git a/token/program-2022/src/error.rs b/token/program-2022/src/error.rs index 7e8b57b93f2..e4f5d34651e 100644 --- a/token/program-2022/src/error.rs +++ b/token/program-2022/src/error.rs @@ -135,9 +135,16 @@ pub enum TokenError { /// mint and try again #[error("An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again")] AccountHasWithheldTransferFees, + /// No memo in previous instruction; required for recipient to receive a transfer #[error("No memo in previous instruction; required for recipient to receive a transfer")] NoMemo, + /// Transfer is disabled for this mint + #[error("Transfer is disabled for this mint")] + NonTransferable, + /// Non-transferable tokens can't be minted to an account without immutable ownership + #[error("Non-transferable tokens can't be minted to an account without immutable ownership")] + NonTransferableNeedsImmutableOwnership, } impl From for ProgramError { fn from(e: TokenError) -> Self { diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index 5cc8802c14f..7f92705775d 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -9,6 +9,7 @@ use { immutable_owner::ImmutableOwner, memo_transfer::MemoTransfer, mint_close_authority::MintCloseAuthority, + non_transferable::NonTransferable, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, }, pod::*, @@ -36,6 +37,8 @@ pub mod immutable_owner; pub mod memo_transfer; /// Mint Close Authority extension pub mod mint_close_authority; +/// Non Transferable extension +pub mod non_transferable; /// Utility to reallocate token accounts pub mod reallocate; /// Transfer Fee extension @@ -599,6 +602,8 @@ pub enum ExtensionType { ImmutableOwner, /// Require inbound transfers to have memo MemoTransfer, + /// Indicates that the tokens from this mint can't be transfered + NonTransferable, /// Padding extension used to make an account exactly Multisig::LEN, used for testing #[cfg(test)] AccountPaddingTest = u16::MAX - 1, @@ -637,6 +642,7 @@ impl ExtensionType { } ExtensionType::DefaultAccountState => pod_get_packed_len::(), ExtensionType::MemoTransfer => pod_get_packed_len::(), + ExtensionType::NonTransferable => pod_get_packed_len::(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -691,7 +697,8 @@ impl ExtensionType { ExtensionType::TransferFeeConfig | ExtensionType::MintCloseAuthority | ExtensionType::ConfidentialTransferMint - | ExtensionType::DefaultAccountState => AccountType::Mint, + | ExtensionType::DefaultAccountState + | ExtensionType::NonTransferable => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount diff --git a/token/program-2022/src/extension/non_transferable.rs b/token/program-2022/src/extension/non_transferable.rs new file mode 100644 index 00000000000..5e454a88f00 --- /dev/null +++ b/token/program-2022/src/extension/non_transferable.rs @@ -0,0 +1,13 @@ +use { + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, +}; + +/// Indicates that the tokens from this mint can't be transfered +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[repr(transparent)] +pub struct NonTransferable; + +impl Extension for NonTransferable { + const TYPE: ExtensionType = ExtensionType::NonTransferable; +} diff --git a/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index f351424943d..b3a0bc3f89d 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -571,6 +571,19 @@ pub enum TokenInstruction<'a> { /// 2. `[]` System program for mint account funding /// CreateNativeMint, + /// Initialize the non transferable extension for the given mint account + /// + /// Fails if the account has already been initialized, so must be called before + /// `InitializeMint`. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint account to initialize. + /// + /// Data expected by this instruction: + /// None + /// + InitializeNonTransferableMint, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html). @@ -699,6 +712,7 @@ impl<'a> TokenInstruction<'a> { } 30 => Self::MemoTransferExtension, 31 => Self::CreateNativeMint, + 32 => Self::InitializeNonTransferableMint, _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -845,6 +859,9 @@ impl<'a> TokenInstruction<'a> { &Self::CreateNativeMint => { buf.push(31); } + &Self::InitializeNonTransferableMint => { + buf.push(32); + } }; buf } @@ -1684,6 +1701,19 @@ pub fn create_native_mint( }) } +/// Creates an `InitializeNonTransferableMint` instruction +pub fn initialize_non_transferable_mint( + token_program_id: &Pubkey, + mint_pubkey: &Pubkey, +) -> Result { + check_program_account(token_program_id)?; + Ok(Instruction { + program_id: *token_program_id, + accounts: vec![AccountMeta::new(*mint_pubkey, false)], + data: TokenInstruction::InitializeNonTransferableMint.pack(), + }) +} + /// Utility function that checks index is between MIN_SIGNERS and MAX_SIGNERS pub fn is_valid_signer_index(index: usize) -> bool { (MIN_SIGNERS..=MAX_SIGNERS).contains(&index) diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index 8de78f80597..94e49ec3c56 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -10,6 +10,7 @@ use { immutable_owner::ImmutableOwner, memo_transfer::{self, check_previous_sibling_instruction_is_memo, memo_required}, mint_close_authority::MintCloseAuthority, + non_transferable::NonTransferable, reallocate, transfer_fee::{self, TransferFeeAmount, TransferFeeConfig}, ExtensionType, StateWithExtensions, StateWithExtensionsMut, @@ -290,6 +291,11 @@ impl Processor { let mint_data = mint_info.try_borrow_data()?; let mint = StateWithExtensions::::unpack(&mint_data)?; + + if mint.get_extension::().is_ok() { + return Err(TokenError::NonTransferable.into()); + } + if expected_decimals != mint.base.decimals { return Err(TokenError::MintDecimalsMismatch.into()); } @@ -694,6 +700,17 @@ impl Processor { let mut mint_data = mint_info.data.borrow_mut(); let mut mint = StateWithExtensionsMut::::unpack(&mut mint_data)?; + + // If the mint if non-transferable, only allow minting to accounts + // with immutable ownership. + if mint.get_extension::().is_ok() + && destination_account + .get_extension::() + .is_err() + { + return Err(TokenError::NonTransferableNeedsImmutableOwnership.into()); + } + if let Some(expected_decimals) = expected_decimals { if expected_decimals != mint.base.decimals { return Err(TokenError::MintDecimalsMismatch.into()); @@ -1111,6 +1128,18 @@ impl Processor { ) } + /// Processes an [InitializeNonTransferableMint](enum.TokenInstruction.html) instruction + pub fn process_initialize_non_transferable_mint(accounts: &[AccountInfo]) -> 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 = StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data)?; + mint.init_extension::()?; + + Ok(()) + } + /// Processes an [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = TokenInstruction::unpack(input)?; @@ -1260,6 +1289,10 @@ impl Processor { msg!("Instruction: CreateNativeMint"); Self::process_create_native_mint(accounts) } + TokenInstruction::InitializeNonTransferableMint => { + msg!("Instruction: InitializeNonTransferableMint"); + Self::process_initialize_non_transferable_mint(accounts) + } } } @@ -1414,6 +1447,12 @@ impl PrintProgramError for TokenError { TokenError::NoMemo => { msg!("Error: No memo in previous instruction; required for recipient to receive a transfer"); } + TokenError::NonTransferable => { + msg!("Transfer is disabled for this mint"); + } + TokenError::NonTransferableNeedsImmutableOwnership => { + msg!("Non-transferable tokens can't be minted to an account without immutable ownership"); + } } } }