diff --git a/token/client/src/token.rs b/token/client/src/token.rs index 20cae87c1fe..30fa7d40318 100644 --- a/token/client/src/token.rs +++ b/token/client/src/token.rs @@ -43,8 +43,8 @@ use { ConfidentialTransferFeeConfig, }, cpi_guard, default_account_state, group_member_pointer, group_pointer, - interest_bearing_mint, memo_transfer, metadata_pointer, scaled_ui_amount, transfer_fee, - transfer_hook, BaseStateWithExtensions, Extension, ExtensionType, + interest_bearing_mint, memo_transfer, metadata_pointer, pausable, scaled_ui_amount, + transfer_fee, transfer_hook, BaseStateWithExtensions, Extension, ExtensionType, StateWithExtensionsOwned, }, instruction, offchain, @@ -193,6 +193,9 @@ pub enum ExtensionInitializationParams { authority: Option, multiplier: f64, }, + PausableConfig { + authority: Pubkey, + }, } impl ExtensionInitializationParams { /// Get the extension type associated with the init params @@ -213,6 +216,7 @@ impl ExtensionInitializationParams { Self::GroupPointer { .. } => ExtensionType::GroupPointer, Self::GroupMemberPointer { .. } => ExtensionType::GroupMemberPointer, Self::ScaledUiAmountConfig { .. } => ExtensionType::ScaledUiAmount, + Self::PausableConfig { .. } => ExtensionType::Pausable, } } /// Generate an appropriate initialization instruction for the given mint @@ -331,6 +335,9 @@ impl ExtensionInitializationParams { authority, multiplier, ), + Self::PausableConfig { authority } => { + pausable::instruction::initialize(token_program_id, mint, &authority) + } } } } @@ -1753,6 +1760,48 @@ where .await } + /// Pause transferring, minting, and burning on the mint + pub async fn pause( + &self, + authority: &Pubkey, + signing_keypairs: &S, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); + + self.process_ixs( + &[pausable::instruction::pause( + &self.program_id, + self.get_address(), + authority, + &multisig_signers, + )?], + signing_keypairs, + ) + .await + } + + /// Resume transferring, minting, and burning on the mint + pub async fn resume( + &self, + authority: &Pubkey, + signing_keypairs: &S, + ) -> TokenResult { + let signing_pubkeys = signing_keypairs.pubkeys(); + let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys); + + self.process_ixs( + &[pausable::instruction::resume( + &self.program_id, + self.get_address(), + authority, + &multisig_signers, + )?], + signing_keypairs, + ) + .await + } + /// Prevent unsafe usage of token account through CPI pub async fn enable_cpi_guard( &self, diff --git a/token/program-2022-test/tests/confidential_transfer.rs b/token/program-2022-test/tests/confidential_transfer.rs index e3c80fe6f44..4d7c8fa9b2c 100644 --- a/token/program-2022-test/tests/confidential_transfer.rs +++ b/token/program-2022-test/tests/confidential_transfer.rs @@ -1617,6 +1617,86 @@ async fn confidential_transfer_transfer() { .await; } +#[cfg(feature = "zk-ops")] +#[tokio::test] +async fn pause_confidential_transfer() { + let authority = Keypair::new(); + let pausable_authority = Keypair::new(); + let auto_approve_new_accounts = true; + let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); + let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); + + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ + ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(authority.pubkey()), + auto_approve_new_accounts, + auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), + }, + ExtensionInitializationParams::PausableConfig { + authority: pausable_authority.pubkey(), + }, + ]) + .await + .unwrap(); + + let TokenContext { + token, + alice, + bob, + mint_authority, + decimals, + .. + } = context.token_context.unwrap(); + + let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( + &token, + &alice, + None, + false, + false, + &mint_authority, + 42, + decimals, + ) + .await; + + let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, Some(2), false, false).await; + + // pause it + token + .pause(&pausable_authority.pubkey(), &[&pausable_authority]) + .await + .unwrap(); + let error = confidential_transfer_with_option( + &token, + &alice_meta.token_account, + &bob_meta.token_account, + &alice.pubkey(), + 10, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + bob_meta.elgamal_keypair.pubkey(), + Some(auditor_elgamal_keypair.pubkey()), + None, + &[&alice], + ConfidentialTransferOption::InstructionData, + ) + .await + .unwrap_err(); + + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintPaused as u32) + ) + ))) + ); +} + #[cfg(feature = "zk-ops")] async fn confidential_transfer_transfer_with_option(option: ConfidentialTransferOption) { let authority = Keypair::new(); @@ -2328,6 +2408,107 @@ async fn confidential_transfer_transfer_with_fee() { .await; } +#[cfg(feature = "zk-ops")] +#[tokio::test] +async fn pause_confidential_transfer_with_fee() { + let transfer_fee_authority = Keypair::new(); + let withdraw_withheld_authority = Keypair::new(); + + let pausable_authority = Keypair::new(); + let confidential_transfer_authority = Keypair::new(); + let auto_approve_new_accounts = true; + let auditor_elgamal_keypair = ElGamalKeypair::new_rand(); + let auditor_elgamal_pubkey = (*auditor_elgamal_keypair.pubkey()).into(); + + let confidential_transfer_fee_authority = Keypair::new(); + let withdraw_withheld_authority_elgamal_keypair = ElGamalKeypair::new_rand(); + let withdraw_withheld_authority_elgamal_pubkey = + (*withdraw_withheld_authority_elgamal_keypair.pubkey()).into(); + + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ + ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: Some(transfer_fee_authority.pubkey()), + withdraw_withheld_authority: Some(withdraw_withheld_authority.pubkey()), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS, + maximum_fee: TEST_MAXIMUM_FEE, + }, + ExtensionInitializationParams::ConfidentialTransferMint { + authority: Some(confidential_transfer_authority.pubkey()), + auto_approve_new_accounts, + auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey), + }, + ExtensionInitializationParams::ConfidentialTransferFeeConfig { + authority: Some(confidential_transfer_fee_authority.pubkey()), + withdraw_withheld_authority_elgamal_pubkey, + }, + ExtensionInitializationParams::PausableConfig { + authority: pausable_authority.pubkey(), + }, + ]) + .await + .unwrap(); + + let TokenContext { + token, + alice, + bob, + mint_authority, + decimals, + .. + } = context.token_context.unwrap(); + + let alice_meta = ConfidentialTokenAccountMeta::new_with_tokens( + &token, + &alice, + None, + false, + true, + &mint_authority, + 100, + decimals, + ) + .await; + + let bob_meta = ConfidentialTokenAccountMeta::new(&token, &bob, None, false, true).await; + + token + .pause(&pausable_authority.pubkey(), &[&pausable_authority]) + .await + .unwrap(); + + let error = confidential_transfer_with_fee_with_option( + &token, + &alice_meta.token_account, + &bob_meta.token_account, + &alice.pubkey(), + 10, + &alice_meta.elgamal_keypair, + &alice_meta.aes_key, + bob_meta.elgamal_keypair.pubkey(), + Some(auditor_elgamal_keypair.pubkey()), + withdraw_withheld_authority_elgamal_keypair.pubkey(), + TEST_FEE_BASIS_POINTS, + TEST_MAXIMUM_FEE, + None, + &[&alice], + ConfidentialTransferOption::InstructionData, + ) + .await + .unwrap_err(); + + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintPaused as u32) + ) + ))) + ); +} + #[cfg(feature = "zk-ops")] async fn confidential_transfer_transfer_with_fee_with_option(option: ConfidentialTransferOption) { let transfer_fee_authority = Keypair::new(); diff --git a/token/program-2022-test/tests/pausable.rs b/token/program-2022-test/tests/pausable.rs new file mode 100644 index 00000000000..724758d413d --- /dev/null +++ b/token/program-2022-test/tests/pausable.rs @@ -0,0 +1,356 @@ +#![cfg(feature = "test-sbf")] + +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::{ + error::TokenError, + extension::{ + pausable::{PausableAccount, PausableConfig}, + BaseStateWithExtensions, + }, + instruction::AuthorityType, + }, + spl_token_client::token::{ExtensionInitializationParams, TokenError as TokenClientError}, + std::convert::TryInto, +}; + +#[tokio::test] +async fn success_initialize() { + let authority = Pubkey::new_unique(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { + authority, + }]) + .await + .unwrap(); + let TokenContext { token, 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)); + assert!(!bool::from(extension.paused)); + + let account = Keypair::new(); + token + .create_auxiliary_token_account(&account, &alice.pubkey()) + .await + .unwrap(); + let state = token.get_account_info(&account.pubkey()).await.unwrap(); + let _ = state.get_extension::().unwrap(); +} + +#[tokio::test] +async fn set_authority() { + let authority = Keypair::new(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { + authority: authority.pubkey(), + }]) + .await + .unwrap(); + let TokenContext { token, .. } = context.token_context.take().unwrap(); + + // success + let new_authority = Keypair::new(); + token + .set_authority( + token.get_address(), + &authority.pubkey(), + Some(&new_authority.pubkey()), + AuthorityType::Pause, + &[&authority], + ) + .await + .unwrap(); + let state = token.get_mint_info().await.unwrap(); + let extension = state.get_extension::().unwrap(); + assert_eq!( + extension.authority, + Some(new_authority.pubkey()).try_into().unwrap(), + ); + token + .pause(&new_authority.pubkey(), &[&new_authority]) + .await + .unwrap(); + let err = token + .pause(&authority.pubkey(), &[&authority]) + .await + .unwrap_err(); + assert_eq!( + err, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::OwnerMismatch as u32) + ) + ))) + ); + + // set to none + token + .set_authority( + token.get_address(), + &new_authority.pubkey(), + None, + AuthorityType::Pause, + &[&new_authority], + ) + .await + .unwrap(); + let state = token.get_mint_info().await.unwrap(); + let extension = state.get_extension::().unwrap(); + assert_eq!(extension.authority, None.try_into().unwrap(),); +} + +#[tokio::test] +async fn pause_mint() { + let authority = Keypair::new(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { + authority: authority.pubkey(), + }]) + .await + .unwrap(); + let TokenContext { + mint_authority, + token, + token_unchecked, + alice, + .. + } = context.token_context.take().unwrap(); + + let alice_account = Keypair::new(); + token + .create_auxiliary_token_account(&alice_account, &alice.pubkey()) + .await + .unwrap(); + let alice_account = alice_account.pubkey(); + + token + .pause(&authority.pubkey(), &[&authority]) + .await + .unwrap(); + + let amount = 10; + let error = token + .mint_to( + &alice_account, + &mint_authority.pubkey(), + amount, + &[&mint_authority], + ) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintPaused as u32) + ) + ))) + ); + + let error = token_unchecked + .mint_to( + &alice_account, + &mint_authority.pubkey(), + amount, + &[&mint_authority], + ) + .await + .unwrap_err(); + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintPaused as u32) + ) + ))) + ); +} + +#[tokio::test] +async fn pause_burn() { + let authority = Keypair::new(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { + authority: authority.pubkey(), + }]) + .await + .unwrap(); + let TokenContext { + mint_authority, + token, + token_unchecked, + alice, + .. + } = context.token_context.take().unwrap(); + + let alice_account = Keypair::new(); + token + .create_auxiliary_token_account(&alice_account, &alice.pubkey()) + .await + .unwrap(); + let alice_account = alice_account.pubkey(); + + let amount = 10; + token + .mint_to( + &alice_account, + &mint_authority.pubkey(), + amount, + &[&mint_authority], + ) + .await + .unwrap(); + + token + .pause(&authority.pubkey(), &[&authority]) + .await + .unwrap(); + + let error = token_unchecked + .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) + .await + .unwrap_err(); + + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintPaused as u32) + ) + ))) + ); + + let error = token + .burn(&alice_account, &alice.pubkey(), 1, &[&alice]) + .await + .unwrap_err(); + + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintPaused as u32) + ) + ))) + ); +} + +#[tokio::test] +async fn pause_transfer() { + let authority = Keypair::new(); + let mut context = TestContext::new().await; + context + .init_token_with_mint(vec![ExtensionInitializationParams::PausableConfig { + authority: authority.pubkey(), + }]) + .await + .unwrap(); + let TokenContext { + mint_authority, + token, + token_unchecked, + alice, + bob, + .. + } = context.token_context.take().unwrap(); + + let alice_account = Keypair::new(); + token + .create_auxiliary_token_account(&alice_account, &alice.pubkey()) + .await + .unwrap(); + let alice_account = alice_account.pubkey(); + + let bob_account = Keypair::new(); + token + .create_auxiliary_token_account(&bob_account, &bob.pubkey()) + .await + .unwrap(); + let bob_account = bob_account.pubkey(); + + let amount = 10; + token + .mint_to( + &alice_account, + &mint_authority.pubkey(), + amount, + &[&mint_authority], + ) + .await + .unwrap(); + + token + .pause(&authority.pubkey(), &[&authority]) + .await + .unwrap(); + + let error = token_unchecked + .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) + .await + .unwrap_err(); + + // need to use checked transfer + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintRequiredForTransfer as u32) + ) + ))) + ); + + let error = token + .transfer(&alice_account, &bob_account, &alice.pubkey(), 1, &[&alice]) + .await + .unwrap_err(); + + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintPaused as u32) + ) + ))) + ); + + let error = token + .transfer_with_fee( + &alice_account, + &bob_account, + &alice.pubkey(), + 1, + 0, + &[&alice], + ) + .await + .unwrap_err(); + + assert_eq!( + error, + TokenClientError::Client(Box::new(TransportError::TransactionError( + TransactionError::InstructionError( + 0, + InstructionError::Custom(TokenError::MintPaused as u32) + ) + ))) + ); +} diff --git a/token/program-2022/src/error.rs b/token/program-2022/src/error.rs index 77f20cf5877..c3ea1578eb5 100644 --- a/token/program-2022/src/error.rs +++ b/token/program-2022/src/error.rs @@ -266,6 +266,9 @@ pub enum TokenError { /// Invalid scale for scaled ui amount #[error("Invalid scale for scaled ui amount")] InvalidScale, + /// Transferring, minting, and burning is paused on this mint + #[error("Transferring, minting, and burning is paused on this mint")] + MintPaused, } impl From for ProgramError { fn from(e: TokenError) -> Self { @@ -459,6 +462,9 @@ impl PrintProgramError for TokenError { TokenError::InvalidScale => { msg!("Invalid scale for scaled ui amount") } + TokenError::MintPaused => { + msg!("Transferring, minting, and burning is paused on this mint"); + } } } } diff --git a/token/program-2022/src/extension/confidential_mint_burn/processor.rs b/token/program-2022/src/extension/confidential_mint_burn/processor.rs index 197bb9165a0..1f63a7c4720 100644 --- a/token/program-2022/src/extension/confidential_mint_burn/processor.rs +++ b/token/program-2022/src/extension/confidential_mint_burn/processor.rs @@ -15,6 +15,7 @@ use { ConfidentialMintBurn, }, confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, + pausable::PausableConfig, BaseStateWithExtensions, BaseStateWithExtensionsMut, PodStateWithExtensionsMut, }, instruction::{decode_instruction_data, decode_instruction_type}, @@ -161,6 +162,11 @@ fn process_confidential_mint( let auditor_elgamal_pubkey = mint .get_extension::()? .auditor_elgamal_pubkey; + if let Ok(extension) = mint.get_extension::() { + if extension.paused.into() { + return Err(TokenError::MintPaused.into()); + } + } let mint_burn_extension = mint.get_extension_mut::()?; let proof_context = verify_mint_proof( @@ -285,6 +291,11 @@ fn process_confidential_burn( let auditor_elgamal_pubkey = mint .get_extension::()? .auditor_elgamal_pubkey; + if let Ok(extension) = mint.get_extension::() { + if extension.paused.into() { + return Err(TokenError::MintPaused.into()); + } + } let mint_burn_extension = mint.get_extension_mut::()?; let proof_context = verify_burn_proof( diff --git a/token/program-2022/src/extension/confidential_transfer/processor.rs b/token/program-2022/src/extension/confidential_transfer/processor.rs index 78aff19a17a..f9a3920b76c 100644 --- a/token/program-2022/src/extension/confidential_transfer/processor.rs +++ b/token/program-2022/src/extension/confidential_transfer/processor.rs @@ -16,6 +16,7 @@ use { EncryptedWithheldAmount, }, memo_transfer::{check_previous_sibling_instruction_is_memo, memo_required}, + pausable::PausableConfig, set_account_type, transfer_fee::TransferFeeConfig, transfer_hook, BaseStateWithExtensions, BaseStateWithExtensionsMut, @@ -397,6 +398,12 @@ fn process_deposit( let mint_data = &mint_info.data.borrow_mut(); let mint = PodStateWithExtensions::::unpack(mint_data)?; + if let Ok(extension) = mint.get_extension::() { + if extension.paused.into() { + return Err(TokenError::MintPaused.into()); + } + } + if expected_decimals != mint.base.decimals { return Err(TokenError::MintDecimalsMismatch.into()); } @@ -518,6 +525,12 @@ fn process_withdraw( return Err(TokenError::IllegalMintBurnConversion.into()); } + if let Ok(extension) = mint.get_extension::() { + if extension.paused.into() { + return Err(TokenError::MintPaused.into()); + } + } + check_program_account(token_account_info.owner)?; let token_account_data = &mut token_account_info.data.borrow_mut(); let mut token_account = PodStateWithExtensionsMut::::unpack(token_account_data)?; @@ -608,6 +621,12 @@ fn process_transfer( let mint_data = mint_info.data.borrow_mut(); let mint = PodStateWithExtensions::::unpack(&mint_data)?; + if let Ok(extension) = mint.get_extension::() { + if extension.paused.into() { + return Err(TokenError::MintPaused.into()); + } + } + let confidential_transfer_mint = mint.get_extension::()?; // A `Transfer` instruction must be accompanied by a zero-knowledge proof diff --git a/token/program-2022/src/extension/mod.rs b/token/program-2022/src/extension/mod.rs index d3f45977041..e68d5b4a618 100644 --- a/token/program-2022/src/extension/mod.rs +++ b/token/program-2022/src/extension/mod.rs @@ -21,6 +21,7 @@ use { metadata_pointer::MetadataPointer, mint_close_authority::MintCloseAuthority, non_transferable::{NonTransferable, NonTransferableAccount}, + pausable::{PausableAccount, PausableConfig}, permanent_delegate::PermanentDelegate, scaled_ui_amount::ScaledUiAmountConfig, transfer_fee::{TransferFeeAmount, TransferFeeConfig}, @@ -73,6 +74,8 @@ pub mod metadata_pointer; pub mod mint_close_authority; /// Non Transferable extension pub mod non_transferable; +/// Pausable extension +pub mod pausable; /// Permanent Delegate extension pub mod permanent_delegate; /// Utility to reallocate token accounts @@ -771,6 +774,9 @@ pub trait BaseStateWithExtensionsMut: BaseStateWithExtensions { // ConfidentialTransfers are currently opt-in only, so this is a no-op for extra safety // on InitializeAccount ExtensionType::ConfidentialTransferAccount => Ok(()), + ExtensionType::PausableAccount => { + self.init_extension::(true).map(|_| ()) + } #[cfg(test)] ExtensionType::AccountPaddingTest => { self.init_extension::(true).map(|_| ()) @@ -1114,6 +1120,10 @@ pub enum ExtensionType { ConfidentialMintBurn, /// Tokens whose UI amount is scaled by a given amount ScaledUiAmount, + /// Tokens where minting / burning / transferring can be paused + Pausable, + /// Indicates that the account belongs to a pausable mint + PausableAccount, /// Test variable-length mint extension #[cfg(test)] @@ -1197,6 +1207,8 @@ impl ExtensionType { ExtensionType::TokenGroupMember => pod_get_packed_len::(), ExtensionType::ConfidentialMintBurn => pod_get_packed_len::(), ExtensionType::ScaledUiAmount => pod_get_packed_len::(), + ExtensionType::Pausable => pod_get_packed_len::(), + ExtensionType::PausableAccount => pod_get_packed_len::(), #[cfg(test)] ExtensionType::AccountPaddingTest => pod_get_packed_len::(), #[cfg(test)] @@ -1262,7 +1274,8 @@ impl ExtensionType { | ExtensionType::GroupMemberPointer | ExtensionType::ConfidentialMintBurn | ExtensionType::TokenGroupMember - | ExtensionType::ScaledUiAmount => AccountType::Mint, + | ExtensionType::ScaledUiAmount + | ExtensionType::Pausable => AccountType::Mint, ExtensionType::ImmutableOwner | ExtensionType::TransferFeeAmount | ExtensionType::ConfidentialTransferAccount @@ -1270,7 +1283,8 @@ impl ExtensionType { | ExtensionType::NonTransferableAccount | ExtensionType::TransferHookAccount | ExtensionType::CpiGuard - | ExtensionType::ConfidentialTransferFeeAmount => AccountType::Account, + | ExtensionType::ConfidentialTransferFeeAmount + | ExtensionType::PausableAccount => AccountType::Account, #[cfg(test)] ExtensionType::VariableLenMintTest => AccountType::Mint, #[cfg(test)] @@ -1296,6 +1310,9 @@ impl ExtensionType { ExtensionType::TransferHook => { account_extension_types.push(ExtensionType::TransferHookAccount); } + ExtensionType::Pausable => { + account_extension_types.push(ExtensionType::PausableAccount); + } #[cfg(test)] ExtensionType::MintPaddingTest => { account_extension_types.push(ExtensionType::AccountPaddingTest); diff --git a/token/program-2022/src/extension/pausable/instruction.rs b/token/program-2022/src/extension/pausable/instruction.rs new file mode 100644 index 00000000000..0cc7c973997 --- /dev/null +++ b/token/program-2022/src/extension/pausable/instruction.rs @@ -0,0 +1,136 @@ +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; +use { + crate::{ + check_program_account, + instruction::{encode_instruction, TokenInstruction}, + }, + bytemuck::{Pod, Zeroable}, + num_enum::{IntoPrimitive, TryFromPrimitive}, + solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + }, +}; + +/// Pausable extension instructions +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub enum PausableInstruction { + /// Initialize the pausable 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: + /// `crate::extension::pausable::instruction::InitializeInstructionData` + Initialize, + /// Pause minting, burning, and transferring for the mint. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint to update. + /// 1. `[signer]` The mint's pause authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint to update. + /// 1. `[]` The mint's multisignature pause authority. + /// 2. `..2+M` `[signer]` M signer accounts. + Pause, + /// Resume minting, burning, and transferring for the mint. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint to update. + /// 1. `[signer]` The mint's pause authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint to update. + /// 1. `[]` The mint's multisignature pause authority. + /// 2. `..2+M` `[signer]` M signer accounts. + Resume, +} + +/// Data expected by `PausableInstruction::Initialize` +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct InitializeInstructionData { + /// The public key for the account that can pause the mint + pub authority: Pubkey, +} + +/// Create an `Initialize` instruction +pub fn initialize( + 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::PausableExtension, + PausableInstruction::Initialize, + &InitializeInstructionData { + authority: *authority, + }, + )) +} + +/// Create a `Pause` instruction +pub fn pause( + 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::PausableExtension, + PausableInstruction::Pause, + &(), + )) +} + +/// Create a `Resume` instruction +pub fn resume( + 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::PausableExtension, + PausableInstruction::Resume, + &(), + )) +} diff --git a/token/program-2022/src/extension/pausable/mod.rs b/token/program-2022/src/extension/pausable/mod.rs new file mode 100644 index 00000000000..82a3878abe4 --- /dev/null +++ b/token/program-2022/src/extension/pausable/mod.rs @@ -0,0 +1,39 @@ +#[cfg(feature = "serde-traits")] +use serde::{Deserialize, Serialize}; +use { + crate::extension::{Extension, ExtensionType}, + bytemuck::{Pod, Zeroable}, + spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodBool}, +}; + +/// Instruction types for the pausable extension +pub mod instruction; +/// Instruction processor for the pausable extension +pub mod processor; + +/// Indicates that the tokens from this mint can be paused +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[repr(C)] +pub struct PausableConfig { + /// Authority that can pause or resume activity on the mint + pub authority: OptionalNonZeroPubkey, + /// Whether minting / transferring / burning tokens is paused + pub paused: PodBool, +} + +/// Indicates that the tokens from this account belong to a pausable mint +#[cfg_attr(feature = "serde-traits", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde-traits", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)] +#[repr(transparent)] +pub struct PausableAccount; + +impl Extension for PausableConfig { + const TYPE: ExtensionType = ExtensionType::Pausable; +} + +impl Extension for PausableAccount { + const TYPE: ExtensionType = ExtensionType::PausableAccount; +} diff --git a/token/program-2022/src/extension/pausable/processor.rs b/token/program-2022/src/extension/pausable/processor.rs new file mode 100644 index 00000000000..60e79ca63f0 --- /dev/null +++ b/token/program-2022/src/extension/pausable/processor.rs @@ -0,0 +1,91 @@ +use { + crate::{ + check_program_account, + error::TokenError, + extension::{ + pausable::{ + instruction::{InitializeInstructionData, PausableInstruction}, + PausableConfig, + }, + BaseStateWithExtensionsMut, PodStateWithExtensionsMut, + }, + instruction::{decode_instruction_data, decode_instruction_type}, + pod::PodMint, + processor::Processor, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + pubkey::Pubkey, + }, +}; + +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 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/token/program-2022/src/instruction.rs b/token/program-2022/src/instruction.rs index c36c666ec58..974fc12c7b4 100644 --- a/token/program-2022/src/instruction.rs +++ b/token/program-2022/src/instruction.rs @@ -715,6 +715,8 @@ pub enum TokenInstruction<'a> { /// Instruction prefix for instructions to the scaled ui amount /// extension ScaledUiAmountExtension, + /// Instruction prefix for instructions to the pausable extension + PausableExtension, } impl<'a> TokenInstruction<'a> { /// Unpacks a byte buffer into a @@ -856,6 +858,7 @@ impl<'a> TokenInstruction<'a> { 41 => Self::GroupMemberPointerExtension, 42 => Self::ConfidentialMintBurnExtension, 43 => Self::ScaledUiAmountExtension, + 44 => Self::PausableExtension, _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -1033,6 +1036,9 @@ impl<'a> TokenInstruction<'a> { &Self::ScaledUiAmountExtension => { buf.push(43); } + &Self::PausableExtension => { + buf.push(44); + } }; buf } @@ -1132,6 +1138,8 @@ pub enum AuthorityType { GroupMemberPointer, /// Authority to set the UI amount scale ScaledUiAmount, + /// Authority to pause or resume minting / transferring / burning + Pause, } impl AuthorityType { @@ -1153,6 +1161,7 @@ impl AuthorityType { AuthorityType::GroupPointer => 13, AuthorityType::GroupMemberPointer => 14, AuthorityType::ScaledUiAmount => 15, + AuthorityType::Pause => 16, } } @@ -1174,6 +1183,7 @@ impl AuthorityType { 13 => Ok(AuthorityType::GroupPointer), 14 => Ok(AuthorityType::GroupMemberPointer), 15 => Ok(AuthorityType::ScaledUiAmount), + 16 => Ok(AuthorityType::Pause), _ => Err(TokenError::InvalidInstruction.into()), } } diff --git a/token/program-2022/src/pod_instruction.rs b/token/program-2022/src/pod_instruction.rs index 05da1e42d4d..4222e1972ba 100644 --- a/token/program-2022/src/pod_instruction.rs +++ b/token/program-2022/src/pod_instruction.rs @@ -116,6 +116,7 @@ pub(crate) enum PodTokenInstruction { GroupMemberPointerExtension, ConfidentialMintBurnExtension, ScaledUiAmountExtension, + PausableExtension, } fn unpack_pubkey_option(input: &[u8]) -> Result, ProgramError> { diff --git a/token/program-2022/src/processor.rs b/token/program-2022/src/processor.rs index 99a813e4775..908d50a0547 100644 --- a/token/program-2022/src/processor.rs +++ b/token/program-2022/src/processor.rs @@ -20,6 +20,7 @@ use { metadata_pointer::{self, MetadataPointer}, mint_close_authority::MintCloseAuthority, non_transferable::{NonTransferable, NonTransferableAccount}, + pausable::{self, PausableAccount, PausableConfig}, permanent_delegate::{get_permanent_delegate, PermanentDelegate}, reallocate, scaled_ui_amount::{self, ScaledUiAmountConfig}, @@ -349,6 +350,12 @@ impl Processor { 0 }; + if let Ok(extension) = mint.get_extension::() { + if extension.paused.into() { + return Err(TokenError::MintPaused.into()); + } + } + let maybe_permanent_delegate = get_permanent_delegate(&mint); let maybe_transfer_hook_program_id = transfer_hook::get_program_id(&mint); @@ -376,6 +383,12 @@ impl Processor { return Err(TokenError::MintRequiredForTransfer.into()); } + // Pausable extension exists on the account, but no mint + // was provided to see if it's paused, abort + if source_account.get_extension::().is_ok() { + return Err(TokenError::MintRequiredForTransfer.into()); + } + (0, None, None) }; if let Some(expected_fee) = expected_fee { @@ -921,6 +934,19 @@ impl Processor { )?; extension.authority = new_authority.try_into()?; } + AuthorityType::Pause => { + 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()); } @@ -972,6 +998,12 @@ impl Processor { return Err(TokenError::NonTransferableNeedsImmutableOwnership.into()); } + if let Ok(extension) = mint.get_extension::() { + if extension.paused.into() { + return Err(TokenError::MintPaused.into()); + } + } + if mint.get_extension::().is_ok() { return Err(TokenError::IllegalMintBurnConversion.into()); } @@ -1053,6 +1085,11 @@ impl Processor { return Err(TokenError::MintDecimalsMismatch.into()); } } + if let Ok(extension) = mint.get_extension::() { + if extension.paused.into() { + return Err(TokenError::MintPaused.into()); + } + } let maybe_permanent_delegate = get_permanent_delegate(&mint); if let Ok(cpi_guard) = source_account.get_extension::() { @@ -1844,6 +1881,10 @@ impl Processor { &input[1..], ) } + PodTokenInstruction::PausableExtension => { + msg!("Instruction: PausableExtension"); + pausable::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)