Skip to content

Commit

Permalink
token-2022: Add Pausable extension
Browse files Browse the repository at this point in the history
#### Problem

Users want a more Ethereum-style token experience by being able to
"pause" their token, similar to the "Pausable" interface. When a mint is
paused, tokens cannot be minted, burned, or transferred.

#### Summary of changes

Add the extension and some tests. It covers the following interactions:

* mint / mint-checked
* burn / burn-checked
* transfer / transfer-checked / transfer-with-fee
* confidential transfer / confidential transfer with fee
* confidential mint / confidential burn

Unfortunately, the confidential mint / burn extension doesn't have
testing, so I couldn't get a full end-to-end test for it.

Also note that it's still possible to:

* move withheld tokens
* initialize token accounts
* close token accounts
* set authority
* freeze / thaw
* approve / revoke
* almost every other bit of extension management
  • Loading branch information
joncinque committed Dec 5, 2024
1 parent 96a1575 commit f914672
Show file tree
Hide file tree
Showing 13 changed files with 961 additions and 4 deletions.
53 changes: 51 additions & 2 deletions token/client/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -193,6 +193,9 @@ pub enum ExtensionInitializationParams {
authority: Option<Pubkey>,
multiplier: f64,
},
PausableConfig {
authority: Pubkey,
},
}
impl ExtensionInitializationParams {
/// Get the extension type associated with the init params
Expand All @@ -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
Expand Down Expand Up @@ -331,6 +335,9 @@ impl ExtensionInitializationParams {
authority,
multiplier,
),
Self::PausableConfig { authority } => {
pausable::instruction::initialize(token_program_id, mint, &authority)
}
}
}
}
Expand Down Expand Up @@ -1753,6 +1760,48 @@ where
.await
}

/// Pause transferring, minting, and burning on the mint
pub async fn pause<S: Signers>(
&self,
authority: &Pubkey,
signing_keypairs: &S,
) -> TokenResult<T::Output> {
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<S: Signers>(
&self,
authority: &Pubkey,
signing_keypairs: &S,
) -> TokenResult<T::Output> {
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<S: Signers>(
&self,
Expand Down
181 changes: 181 additions & 0 deletions token/program-2022-test/tests/confidential_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit f914672

Please sign in to comment.