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

Commit f1c693d

Browse files
mvinesJuan Oxoby
andauthored
token-2022: Add support for Non-Transferable Tokens (NTTs) - NonTransferableMint extension (#3178)
Co-authored-by: Juan Oxoby <[email protected]>
1 parent 791cc8a commit f1c693d

File tree

5 files changed

+97
-1
lines changed

5 files changed

+97
-1
lines changed

token/program-2022/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,16 @@ pub enum TokenError {
135135
/// mint and try again
136136
#[error("An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again")]
137137
AccountHasWithheldTransferFees,
138+
138139
/// No memo in previous instruction; required for recipient to receive a transfer
139140
#[error("No memo in previous instruction; required for recipient to receive a transfer")]
140141
NoMemo,
142+
/// Transfer is disabled for this mint
143+
#[error("Transfer is disabled for this mint")]
144+
NonTransferable,
145+
/// Non-transferable tokens can't be minted to an account without immutable ownership
146+
#[error("Non-transferable tokens can't be minted to an account without immutable ownership")]
147+
NonTransferableNeedsImmutableOwnership,
141148
}
142149
impl From<TokenError> for ProgramError {
143150
fn from(e: TokenError) -> Self {

token/program-2022/src/extension/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use {
99
immutable_owner::ImmutableOwner,
1010
memo_transfer::MemoTransfer,
1111
mint_close_authority::MintCloseAuthority,
12+
non_transferable::NonTransferable,
1213
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
1314
},
1415
pod::*,
@@ -36,6 +37,8 @@ pub mod immutable_owner;
3637
pub mod memo_transfer;
3738
/// Mint Close Authority extension
3839
pub mod mint_close_authority;
40+
/// Non Transferable extension
41+
pub mod non_transferable;
3942
/// Utility to reallocate token accounts
4043
pub mod reallocate;
4144
/// Transfer Fee extension
@@ -599,6 +602,8 @@ pub enum ExtensionType {
599602
ImmutableOwner,
600603
/// Require inbound transfers to have memo
601604
MemoTransfer,
605+
/// Indicates that the tokens from this mint can't be transfered
606+
NonTransferable,
602607
/// Padding extension used to make an account exactly Multisig::LEN, used for testing
603608
#[cfg(test)]
604609
AccountPaddingTest = u16::MAX - 1,
@@ -637,6 +642,7 @@ impl ExtensionType {
637642
}
638643
ExtensionType::DefaultAccountState => pod_get_packed_len::<DefaultAccountState>(),
639644
ExtensionType::MemoTransfer => pod_get_packed_len::<MemoTransfer>(),
645+
ExtensionType::NonTransferable => pod_get_packed_len::<NonTransferable>(),
640646
#[cfg(test)]
641647
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
642648
#[cfg(test)]
@@ -691,7 +697,8 @@ impl ExtensionType {
691697
ExtensionType::TransferFeeConfig
692698
| ExtensionType::MintCloseAuthority
693699
| ExtensionType::ConfidentialTransferMint
694-
| ExtensionType::DefaultAccountState => AccountType::Mint,
700+
| ExtensionType::DefaultAccountState
701+
| ExtensionType::NonTransferable => AccountType::Mint,
695702
ExtensionType::ImmutableOwner
696703
| ExtensionType::TransferFeeAmount
697704
| ExtensionType::ConfidentialTransferAccount
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use {
2+
crate::extension::{Extension, ExtensionType},
3+
bytemuck::{Pod, Zeroable},
4+
};
5+
6+
/// Indicates that the tokens from this mint can't be transfered
7+
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
8+
#[repr(transparent)]
9+
pub struct NonTransferable;
10+
11+
impl Extension for NonTransferable {
12+
const TYPE: ExtensionType = ExtensionType::NonTransferable;
13+
}

token/program-2022/src/instruction.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,19 @@ pub enum TokenInstruction<'a> {
571571
/// 2. `[]` System program for mint account funding
572572
///
573573
CreateNativeMint,
574+
/// Initialize the non transferable extension for the given mint account
575+
///
576+
/// Fails if the account has already been initialized, so must be called before
577+
/// `InitializeMint`.
578+
///
579+
/// Accounts expected by this instruction:
580+
///
581+
/// 0. `[writable]` The mint account to initialize.
582+
///
583+
/// Data expected by this instruction:
584+
/// None
585+
///
586+
InitializeNonTransferableMint,
574587
}
575588
impl<'a> TokenInstruction<'a> {
576589
/// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html).
@@ -699,6 +712,7 @@ impl<'a> TokenInstruction<'a> {
699712
}
700713
30 => Self::MemoTransferExtension,
701714
31 => Self::CreateNativeMint,
715+
32 => Self::InitializeNonTransferableMint,
702716
_ => return Err(TokenError::InvalidInstruction.into()),
703717
})
704718
}
@@ -845,6 +859,9 @@ impl<'a> TokenInstruction<'a> {
845859
&Self::CreateNativeMint => {
846860
buf.push(31);
847861
}
862+
&Self::InitializeNonTransferableMint => {
863+
buf.push(32);
864+
}
848865
};
849866
buf
850867
}
@@ -1684,6 +1701,19 @@ pub fn create_native_mint(
16841701
})
16851702
}
16861703

1704+
/// Creates an `InitializeNonTransferableMint` instruction
1705+
pub fn initialize_non_transferable_mint(
1706+
token_program_id: &Pubkey,
1707+
mint_pubkey: &Pubkey,
1708+
) -> Result<Instruction, ProgramError> {
1709+
check_program_account(token_program_id)?;
1710+
Ok(Instruction {
1711+
program_id: *token_program_id,
1712+
accounts: vec![AccountMeta::new(*mint_pubkey, false)],
1713+
data: TokenInstruction::InitializeNonTransferableMint.pack(),
1714+
})
1715+
}
1716+
16871717
/// Utility function that checks index is between MIN_SIGNERS and MAX_SIGNERS
16881718
pub fn is_valid_signer_index(index: usize) -> bool {
16891719
(MIN_SIGNERS..=MAX_SIGNERS).contains(&index)

token/program-2022/src/processor.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use {
1010
immutable_owner::ImmutableOwner,
1111
memo_transfer::{self, check_previous_sibling_instruction_is_memo, memo_required},
1212
mint_close_authority::MintCloseAuthority,
13+
non_transferable::NonTransferable,
1314
reallocate,
1415
transfer_fee::{self, TransferFeeAmount, TransferFeeConfig},
1516
ExtensionType, StateWithExtensions, StateWithExtensionsMut,
@@ -290,6 +291,11 @@ impl Processor {
290291

291292
let mint_data = mint_info.try_borrow_data()?;
292293
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
294+
295+
if mint.get_extension::<NonTransferable>().is_ok() {
296+
return Err(TokenError::NonTransferable.into());
297+
}
298+
293299
if expected_decimals != mint.base.decimals {
294300
return Err(TokenError::MintDecimalsMismatch.into());
295301
}
@@ -694,6 +700,17 @@ impl Processor {
694700

695701
let mut mint_data = mint_info.data.borrow_mut();
696702
let mut mint = StateWithExtensionsMut::<Mint>::unpack(&mut mint_data)?;
703+
704+
// If the mint if non-transferable, only allow minting to accounts
705+
// with immutable ownership.
706+
if mint.get_extension::<NonTransferable>().is_ok()
707+
&& destination_account
708+
.get_extension::<ImmutableOwner>()
709+
.is_err()
710+
{
711+
return Err(TokenError::NonTransferableNeedsImmutableOwnership.into());
712+
}
713+
697714
if let Some(expected_decimals) = expected_decimals {
698715
if expected_decimals != mint.base.decimals {
699716
return Err(TokenError::MintDecimalsMismatch.into());
@@ -1111,6 +1128,18 @@ impl Processor {
11111128
)
11121129
}
11131130

1131+
/// Processes an [InitializeNonTransferableMint](enum.TokenInstruction.html) instruction
1132+
pub fn process_initialize_non_transferable_mint(accounts: &[AccountInfo]) -> ProgramResult {
1133+
let account_info_iter = &mut accounts.iter();
1134+
let mint_account_info = next_account_info(account_info_iter)?;
1135+
1136+
let mut mint_data = mint_account_info.data.borrow_mut();
1137+
let mut mint = StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut mint_data)?;
1138+
mint.init_extension::<NonTransferable>()?;
1139+
1140+
Ok(())
1141+
}
1142+
11141143
/// Processes an [Instruction](enum.Instruction.html).
11151144
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
11161145
let instruction = TokenInstruction::unpack(input)?;
@@ -1260,6 +1289,10 @@ impl Processor {
12601289
msg!("Instruction: CreateNativeMint");
12611290
Self::process_create_native_mint(accounts)
12621291
}
1292+
TokenInstruction::InitializeNonTransferableMint => {
1293+
msg!("Instruction: InitializeNonTransferableMint");
1294+
Self::process_initialize_non_transferable_mint(accounts)
1295+
}
12631296
}
12641297
}
12651298

@@ -1414,6 +1447,12 @@ impl PrintProgramError for TokenError {
14141447
TokenError::NoMemo => {
14151448
msg!("Error: No memo in previous instruction; required for recipient to receive a transfer");
14161449
}
1450+
TokenError::NonTransferable => {
1451+
msg!("Transfer is disabled for this mint");
1452+
}
1453+
TokenError::NonTransferableNeedsImmutableOwnership => {
1454+
msg!("Non-transferable tokens can't be minted to an account without immutable ownership");
1455+
}
14171456
}
14181457
}
14191458
}

0 commit comments

Comments
 (0)