Skip to content

Commit

Permalink
token-2022: Add support for Non-Transferable Tokens (NTTs) - NonTrans…
Browse files Browse the repository at this point in the history
…ferableMint extension (solana-labs#3178)

Co-authored-by: Juan Oxoby <me@jmoxo.by>
  • Loading branch information
mvines and Juan Oxoby authored May 17, 2022
1 parent 791cc8a commit f1c693d
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 1 deletion.
7 changes: 7 additions & 0 deletions token/program-2022/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TokenError> for ProgramError {
fn from(e: TokenError) -> Self {
Expand Down
9 changes: 8 additions & 1 deletion token/program-2022/src/extension/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use {
immutable_owner::ImmutableOwner,
memo_transfer::MemoTransfer,
mint_close_authority::MintCloseAuthority,
non_transferable::NonTransferable,
transfer_fee::{TransferFeeAmount, TransferFeeConfig},
},
pod::*,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -637,6 +642,7 @@ impl ExtensionType {
}
ExtensionType::DefaultAccountState => pod_get_packed_len::<DefaultAccountState>(),
ExtensionType::MemoTransfer => pod_get_packed_len::<MemoTransfer>(),
ExtensionType::NonTransferable => pod_get_packed_len::<NonTransferable>(),
#[cfg(test)]
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
#[cfg(test)]
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions token/program-2022/src/extension/non_transferable.rs
Original file line number Diff line number Diff line change
@@ -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;
}
30 changes: 30 additions & 0 deletions token/program-2022/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -699,6 +712,7 @@ impl<'a> TokenInstruction<'a> {
}
30 => Self::MemoTransferExtension,
31 => Self::CreateNativeMint,
32 => Self::InitializeNonTransferableMint,
_ => return Err(TokenError::InvalidInstruction.into()),
})
}
Expand Down Expand Up @@ -845,6 +859,9 @@ impl<'a> TokenInstruction<'a> {
&Self::CreateNativeMint => {
buf.push(31);
}
&Self::InitializeNonTransferableMint => {
buf.push(32);
}
};
buf
}
Expand Down Expand Up @@ -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<Instruction, ProgramError> {
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)
Expand Down
39 changes: 39 additions & 0 deletions token/program-2022/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -290,6 +291,11 @@ impl Processor {

let mint_data = mint_info.try_borrow_data()?;
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;

if mint.get_extension::<NonTransferable>().is_ok() {
return Err(TokenError::NonTransferable.into());
}

if expected_decimals != mint.base.decimals {
return Err(TokenError::MintDecimalsMismatch.into());
}
Expand Down Expand Up @@ -694,6 +700,17 @@ impl Processor {

let mut mint_data = mint_info.data.borrow_mut();
let mut mint = StateWithExtensionsMut::<Mint>::unpack(&mut mint_data)?;

// If the mint if non-transferable, only allow minting to accounts
// with immutable ownership.
if mint.get_extension::<NonTransferable>().is_ok()
&& destination_account
.get_extension::<ImmutableOwner>()
.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());
Expand Down Expand Up @@ -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::<Mint>::unpack_uninitialized(&mut mint_data)?;
mint.init_extension::<NonTransferable>()?;

Ok(())
}

/// Processes an [Instruction](enum.Instruction.html).
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let instruction = TokenInstruction::unpack(input)?;
Expand Down Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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");
}
}
}
}
Expand Down

0 comments on commit f1c693d

Please sign in to comment.