Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions clients/rust-legacy/tests/confidential_mint_burn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1095,3 +1095,100 @@ async fn pause_confidential_mint_burn() {
)))
);
}

#[tokio::test]
async fn fail_close_mint_with_confidential_supply() {
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 supply_elgamal_keypair = ElGamalKeypair::new_rand();
let supply_elgamal_pubkey = (*supply_elgamal_keypair.pubkey()).into();
let supply_aes_key = AeKey::new_rand();
let decryptable_supply = supply_aes_key.encrypt(0).into();

let mut context = TestContext::new().await;
context
.init_token_with_mint(vec![
ExtensionInitializationParams::MintCloseAuthority {
close_authority: Some(confidential_transfer_authority.pubkey()),
},
ExtensionInitializationParams::ConfidentialTransferMint {
authority: Some(confidential_transfer_authority.pubkey()),
auto_approve_new_accounts,
auditor_elgamal_pubkey: Some(auditor_elgamal_pubkey),
},
ExtensionInitializationParams::ConfidentialMintBurn {
supply_elgamal_pubkey,
decryptable_supply,
},
])
.await
.unwrap();

let TokenContext {
token,
mint_authority,
alice,
..
} = context.token_context.unwrap();

let alice_meta = ConfidentialTokenAccountMeta::new(&token, &alice).await;
let mint_amount = 100;

// Mint confidential tokens to Alice. This increases the confidential supply.
token
.confidential_transfer_mint(
&mint_authority.pubkey(),
&alice_meta.token_account,
None,
None,
None,
mint_amount,
&supply_elgamal_keypair,
alice_meta.elgamal_keypair.pubkey(),
Some(auditor_elgamal_keypair.pubkey()),
&supply_aes_key,
None,
&[&mint_authority],
)
.await
.unwrap();

// Apply pending balance to finalize the mint
token
.confidential_transfer_apply_pending_balance(
&alice_meta.token_account,
&alice.pubkey(),
None,
alice_meta.elgamal_keypair.secret(),
&alice_meta.aes_key,
&[&alice],
)
.await
.unwrap();

// Attempt to close the mint.
// This should fail because the confidential supply is non-zero (100).
let err = token
.close_account(
token.get_address(),
&alice.pubkey(),
&confidential_transfer_authority.pubkey(),
&[&confidential_transfer_authority],
)
.await
.unwrap_err();

// 4. Assert that the error is MintHasSupply
assert_eq!(
err,
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(
0,
InstructionError::Custom(TokenError::MintHasSupply as u32)
)
)))
);
}
17 changes: 16 additions & 1 deletion interface/src/extension/confidential_mint_burn/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use {
crate::extension::{Extension, ExtensionType},
crate::{
error::TokenError,
extension::{Extension, ExtensionType},
},
bytemuck::{Pod, Zeroable},
solana_program_error::ProgramResult,
solana_zk_sdk::encryption::pod::{
auth_encryption::PodAeCiphertext,
elgamal::{PodElGamalCiphertext, PodElGamalPubkey},
Expand All @@ -27,3 +31,14 @@ pub struct ConfidentialMintBurn {
impl Extension for ConfidentialMintBurn {
const TYPE: ExtensionType = ExtensionType::ConfidentialMintBurn;
}

impl ConfidentialMintBurn {
/// Checks if the mint can be closed based on confidential supply state
pub fn closable(&self) -> ProgramResult {
if self.confidential_supply == PodElGamalCiphertext::default() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies if this is a silly question, but does this work if someone has minted and then burned tokens?

For example, if I mint 10 tokens, and then burn those 10 tokens, there are 0 tokens, but confidential_supply won't be PodElGamalCiphertext::default(), correct? Is it even possible to reset the confidential_supply to PodElGamalCiphertext::default()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is a totally valid question. If we mint and then burn, then the ciphertext will most likely not be identically zero.

There is a RotateSupplyElGamalPubkey instruction that allows the authority to provide a new ciphertext for encrypted supply. The main purpose is to actually rotate the supply ElGamal public key, but it can be used to update the ciphertext as well. In the instruction, one has to provide:

  • a new ElGamal public key
  • a new ciphertext that encrypts the same supply under the new ElGamal public key
  • a ciphertext-ciphertext equality proof certifying the above

To close the mint, one can provide the same ElGamal public key and an identically zero ciphertext (assuming that the supply is zero already).

I can write a clarifying comment on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah very smart! I didn't realize that was possible with RotateSupplyElGamalPubkey. This is all good then

Ok(())
} else {
Err(TokenError::MintHasSupply.into())
}
}
}
4 changes: 4 additions & 0 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,10 @@ impl Processor {
if u64::from(mint.base.supply) != 0 {
return Err(TokenError::MintHasSupply.into());
}

if let Ok(confidential_mint_burn) = mint.get_extension::<ConfidentialMintBurn>() {
confidential_mint_burn.closable()?;
}
} else {
return Err(ProgramError::UninitializedAccount);
}
Expand Down