No. Token extensions must be configured at mint creation. However, you can use the Token Wrap Program to create a wrapped version with confidential transfers enabled.
Caveat: Wrapped tokens are recognized as separate mints, which fragments liquidity across the ecosystem.
| Balance | Description | Can Transfer? |
|---|---|---|
| Pending | Incoming deposits/transfers waiting to be processed | No |
| Available | Processed balance ready to spend | Yes |
The two-stage model prevents front-running attacks on ZK proofs.
ZK proofs are large. A single confidential transfer requires:
- Range proof (~1,400 bytes for U128)
- Equality proof (~192 bytes)
- Ciphertext validity proof (~224 bytes)
These exceed Solana's 1,232-byte transaction limit, requiring proof data to be stored in separate accounts.
Partially. PDAs face two challenges:
- Key management: ElGamal keys must be stored somewhere. On-chain storage exposes them.
- Proof generation: ZK proofs require the secret key and are computationally expensive.
Solution: Generate proofs client-side, have PDA authorize the transfer after verification.
Permanent loss of confidential balance. The balance cannot be recovered without the secret key.
Keys derived from wallet signature can be regenerated if you have the wallet. Custom keys require secure backup.
Causes:
- Insufficient balance for transfer amount
- Mismatched keypairs (using wrong account's keys)
- Incorrect opening values
Debug:
// Verify you have sufficient balance
let available = decrypt_balance(&ct_account.available_balance, &elgamal_keypair)?;
assert!(available >= transfer_amount, "Insufficient balance");
// Verify correct keypair derivation
let expected_pubkey = ct_account.elgamal_pubkey;
let derived_pubkey = elgamal_keypair.pubkey();
assert_eq!(expected_pubkey, derived_pubkey.into(), "Wrong keypair");Cause: Trying to include proof data directly in transaction.
Solution: Use context state accounts:
// Instead of inline proof:
let proof_location = ProofLocation::InstructionOffset(...);
// Use context account:
token.confidential_transfer_create_context_state_account(
&proof_account_pubkey,
&authority,
&proof_data,
true, // split proof for large data
&signers,
).await?;Causes:
- Wrong secret key
- Amount exceeds u32 range (for
decrypt_u32) - Corrupted ciphertext
Debug:
// Try full decryption for large amounts
let discrete_log = ciphertext.decrypt(&secret_key);
match discrete_log.decode_u32() {
Some(amount) => println!("Amount: {}", amount),
None => println!("Amount exceeds u32 or decryption failed"),
}Cause: Credit counter mismatch.
Solution: Re-fetch account state and use current counter:
let account_info = token.get_account_info(&token_account).await?;
let ct_extension = account_info.get_extension::<ConfidentialTransferAccount>()?;
let apply_info = ApplyPendingBalanceAccountInfo::new(ct_extension);
// Use fresh counter value
let expected_counter = apply_info.pending_balance_credit_counter();Cause: Reusing proof account address.
Solution: Generate fresh keypair for each proof:
// Each transfer needs new proof accounts
let equality_proof_account = Keypair::new();
let range_proof_account = Keypair::new();
let validity_proof_account = Keypair::new();Cause: Using crypto functions before init() in JavaScript/WASM context.
Solution (for JavaScript users):
import init from '@solana/zk-sdk/web';
// Always await init before any crypto
await init();
// Now safe to use
const keypair = new ElGamalKeypair();Rust users: No initialization needed - just use the crates directly.
Cause: Using number literals instead of BigInt in JavaScript.
Solution (for JavaScript users):
// Wrong
const amount = 1000;
// Correct
const amount = 1000n;
// or
const amount = BigInt(1000);Rust users: Use native u64 type - no BigInt needed.
Cause: @solana/zk-sdk is ~8MB due to WASM - affects web applications.
Solutions (for JavaScript users):
- Lazy load the crypto module
- Use code splitting
- Load WASM from CDN
// Lazy load
const zkSdk = await import('@solana/zk-sdk/web');
await zkSdk.default(); // initRust users: Compile natively - no bundle size concerns.
Problem: Some proof transactions succeeded but transfer failed.
Recovery options:
- Roll forward: Retry the transfer instruction
- Roll back: Close proof accounts to reclaim rent
// Close orphaned proof accounts
for proof_account in orphaned_accounts {
token.confidential_transfer_close_context_state_account(
&proof_account,
&destination,
&authority,
&signers,
).await?;
}Causes:
- Insufficient tip
- Validator not running Jito
- Transaction simulation failed
Solutions:
- Increase tip amount
- Retry with fresh blockhash
- Check simulation errors before bundling
Cause: Proof generation took too long.
Solution: Generate proofs first, then build transactions:
// 1. Generate all proofs (slow)
let proofs = generate_transfer_proofs(...)?;
// 2. Get fresh blockhash (fast)
let blockhash = client.get_latest_blockhash()?;
// 3. Build and sign transactions (fast)
let transactions = build_transfer_transactions(proofs, blockhash)?;
// 4. Submit immediately
submit_transactions(transactions)?;Expected times (varies by hardware):
- Range proof: 500ms - 2s
- Equality proof: 50ms - 200ms
- Validity proof: 50ms - 200ms
Optimizations:
- Generate proofs in parallel (where possible)
- Use native Rust instead of WASM
- Pre-compute proofs before user action
Cause: Discrete log computation for amounts > 2^32.
Solutions:
- Use
decryptable_available_balance(AES) for display - Keep amounts within u32 range when possible
- Use
decrypt_u32for known-small values
Typical CU usage:
| Operation | CU |
|---|---|
| Deposit | ~15,000 |
| Apply | ~17,000 |
| Transfer | ~31,000 |
| Withdraw | ~10,000 |
Solutions:
- Request higher CU limit:
SetComputeUnitLimit - Split operations across transactions
- Use context accounts (cheaper than inline proofs)
Rust:
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("debug")
).init();JavaScript (browser):
localStorage.setItem('debug', 'solana:*');# CLI
spl-token display <TOKEN_ACCOUNT>
# Shows confidential transfer extension fieldsRust:
proof_data.verify()?; // Throws if invalidJavaScript:
proof.verify(); // Throws if invalid