diff --git a/Makefile b/Makefile index 6fb9eb598..5aea12a19 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,8 @@ audit: --ignore RUSTSEC-2026-0098 \ --ignore RUSTSEC-2026-0099 \ --ignore RUSTSEC-2026-0104 \ + --ignore RUSTSEC-2026-0173 \ + --ignore RUSTSEC-2026-0258 \ $(ARGS) spellcheck: diff --git a/clients/js/src/confidentialTransferHelpers.ts b/clients/js/src/confidentialTransferHelpers.ts index 7a82e1e46..f86b0e53c 100644 --- a/clients/js/src/confidentialTransferHelpers.ts +++ b/clients/js/src/confidentialTransferHelpers.ts @@ -1538,7 +1538,14 @@ async function buildConfidentialMintProofPlan( const amount = BigInt(input.amount); assertMintBurnAmount(amount, 'Mint'); - const currentSupply = input.supplyAesKey.decrypt(parseAeCiphertext(mintBurnExtension.decryptableSupply)); + // The new-supply commitment must commit to the same value the new-supply + // ciphertext encrypts (`confidential_supply + amount`); the equality proof + // binds the two. Decrypting `confidentialSupply` (rather than the AES + // `decryptableSupply`) keeps the proof valid even after an apply-pending-burn + // advances the encrypted supply without re-encrypting the decryptable one. + const currentSupply = input.supplyElgamalKeypair + .secret() + .decrypt(parseElGamalCiphertext(mintBurnExtension.confidentialSupply)); const newSupply = currentSupply + amount; assertU64Amount(newSupply, 'New supply after mint'); diff --git a/clients/js/test/extensions/confidentialMintBurn/getConfidentialMintBurnInstructionPlan.test.ts b/clients/js/test/extensions/confidentialMintBurn/getConfidentialMintBurnInstructionPlan.test.ts index 9458c716c..e2e928c39 100644 --- a/clients/js/test/extensions/confidentialMintBurn/getConfidentialMintBurnInstructionPlan.test.ts +++ b/clients/js/test/extensions/confidentialMintBurn/getConfidentialMintBurnInstructionPlan.test.ts @@ -19,6 +19,7 @@ import { const DECIMALS = 2; const MINT_AMOUNT = 500n; const BURN_AMOUNT = 200n; +const SECOND_MINT_AMOUNT = 100n; it('confidentially mints into, applies, burns from, and re-syncs the supply of a mint-burn mint', async () => { // Given a mint-burn mint (both ConfidentialTransferMint + ConfidentialMintBurn) @@ -122,3 +123,119 @@ it('confidentially mints into, applies, burns from, and re-syncs the supply of a expect(await fetchDecryptableSupply({ client, mint, supplyAesKey })).toBe(MINT_AMOUNT - BURN_AMOUNT); }); + +it('confidentially mints again after a pending burn without a manual decryptable-supply re-sync', async () => { + // Given a mint-burn mint and a confidential token account, with a pending + // burn applied so that the encrypted supply no longer matches the AES + // decryptable supply. + const client = await createValidatorClient({ estimateResourceLimits: false }); + const payer = client.payer; + const owner = await generateKeyPairSignerWithSol(client); + const { mint, mintAuthority, supplyElgamalKeypair, supplyAesKey } = await createConfidentialMintBurnMint({ + client, + payer, + decimals: DECIMALS, + }); + const account = await createConfidentialTokenAccount({ client, payer, owner, mint }); + + const [{ data: destinationTokenAccount }, { data: mintAccount }] = await Promise.all([ + fetchToken(client.rpc, account.token), + fetchMint(client.rpc, mint), + ]); + await client.sendTransactions( + await getConfidentialMintInstructionPlan({ + payer, + rpc: client.rpc, + token: account.token, + mint, + mintAccount, + destinationTokenAccount, + authority: mintAuthority, + amount: MINT_AMOUNT, + supplyElgamalKeypair, + supplyAesKey, + }), + ); + + const { data: afterMint } = await fetchToken(client.rpc, account.token); + await client.sendTransaction([ + getApplyConfidentialPendingBalanceInstructionFromToken({ + token: account.token, + tokenAccount: afterMint, + authority: owner, + elgamalSecretKey: account.elgamalKeypair.secret(), + aesKey: account.aesKey, + }), + ]); + + const [{ data: sourceTokenAccount }, { data: mintForBurn }] = await Promise.all([ + fetchToken(client.rpc, account.token), + fetchMint(client.rpc, mint), + ]); + await client.sendTransactions( + await getConfidentialBurnInstructionPlan({ + payer, + rpc: client.rpc, + token: account.token, + mint, + mintAccount: mintForBurn, + sourceTokenAccount, + authority: owner, + amount: BURN_AMOUNT, + sourceElgamalKeypair: account.elgamalKeypair, + aesKey: account.aesKey, + }), + ); + + // Apply the pending burn WITHOUT re-syncing the decryptable supply; on-chain + // this advances the encrypted supply only. + await client.sendTransaction([getApplyConfidentialPendingBurnInstruction({ mint, authority: mintAuthority })]); + + // When the authority mints again without a manual decryptable re-sync, the + // equality proof must still bind the new-supply ciphertext to a commitment + // over the confidential (not decryptable) supply, so the mint succeeds. + const [{ data: destinationAfterBurn }, { data: mintAfterBurn }] = await Promise.all([ + fetchToken(client.rpc, account.token), + fetchMint(client.rpc, mint), + ]); + await client.sendTransactions( + await getConfidentialMintInstructionPlan({ + payer, + rpc: client.rpc, + token: account.token, + mint, + mintAccount: mintAfterBurn, + destinationTokenAccount: destinationAfterBurn, + authority: mintAuthority, + amount: SECOND_MINT_AMOUNT, + supplyElgamalKeypair, + supplyAesKey, + }), + ); + + // And the account's available balance decrypts to the full minted amount. + const { data: afterSecondMint } = await fetchToken(client.rpc, account.token); + await client.sendTransaction([ + getApplyConfidentialPendingBalanceInstructionFromToken({ + token: account.token, + tokenAccount: afterSecondMint, + authority: owner, + elgamalSecretKey: account.elgamalKeypair.secret(), + aesKey: account.aesKey, + }), + ]); + const { data: appliedSecondMint } = await fetchToken(client.rpc, account.token); + expect( + decryptConfidentialTransferBalance({ + tokenAccount: appliedSecondMint, + elgamalSecretKey: account.elgamalKeypair.secret(), + aesKey: account.aesKey, + }).availableBalance, + ).toBe(MINT_AMOUNT - BURN_AMOUNT + SECOND_MINT_AMOUNT); + + // Then the decryptable supply tracks the encrypted supply advanced by the + // burn and the second mint. + expect(await fetchDecryptableSupply({ client, mint, supplyAesKey })).toBe( + MINT_AMOUNT - BURN_AMOUNT + SECOND_MINT_AMOUNT, + ); +});