Confidential Asset (CA)
The Confidential Asset (CA) standard allows any fungible asset (FA) type to be wrapped into a variant with certain confidentiality features.
Namely, it allows users to establish a confidential balance and later confidentially-transfer hidden amounts from their confidential balance to another user’s confidential balance. Importantly, such confidential transfers completely hide the transferred amount from everyone, including from Aptos validators. By design, a key limitation of CAs is that they do not hide the sender and recipient addresses.
CAs leverage zero-knowledge proofs (ZKPs) to enable Aptos validators to verify transaction correctness without revealing the hidden transferred amounts nor the confidential balances of the sender and recipient.
Confidential balance
Section titled “Confidential balance”Each confidential balance is split into two parts:
-
pending balance — the received balance, accumulates all incoming deposits and confidential transfers.
-
available balance — the spendable balance, used exclusively for sending outgoing transfers and withdrawing.
Both balances are encrypted with the user’s encryption key (EK), ensuring underlying amounts remain private.
Chunks
Section titled “Chunks”Confidential balances handle token amounts by splitting them into smaller units called chunks. Each chunk represents a portion of the total amount and is encrypted individually using the user’s EK. Each encrypted chunk consists of two elliptic curve points forming a Twisted ElGamal ciphertext.
| Confidential balance type | Chunks | Max chunk size | Max encoded value |
|---|---|---|---|
| pending | 4 × 16-bit | 32 bits | |
| available | 8 × 16-bit | 32 bits |
Both balance types share a single CompressedBalance<T> struct, parameterized by a phantom marker (Pending or Available):
struct Pending has drop {}struct Available has drop {}
enum CompressedBalance<phantom T> has store, drop, copy { V1 { P: vector<CompressedRistretto>, R: vector<CompressedRistretto>, R_aud: vector<CompressedRistretto>, }}Rollover
Section titled “Rollover”Funds in the pending balance cannot be spent directly — they must be rolled over into the available balance first. Additionally, the pending balance can only accumulate up to transfers before a rollover is enforced by the protocol. Note that, by that point, pending balance chunks can reach up to 32 bits each. Crucially, our design ensures that after rolling over into the available balance, the resulting available balance chunks also remain 32-bit. This ensures decryption times are fast.
Normalization
Section titled “Normalization”After a rollover, the available balance must be normalized before further rollovers can occur. Normalization re-packs available balance chunks that may have reached 32 bits back to 16-bit chunks.
Normalization serves two purposes:
- Keeps decryption efficient — smaller chunks means faster decryption.
- Enables further rollovers — rollovers require a normalized available balance. (Otherwise, available balance chunks would exceed 32 bits after a rollover.)
Transfers and withdrawals implicitly normalize the available balance. Users only need to normalize their available balance manually if the following conditions all hold:
- It is not already normalized.
- They need to call
rollover_pending_balance(orrollover_pending_balance_and_pause), which requires a normalized available balance first.
Encryption and decryption
Section titled “Encryption and decryption”As hinted in the Chunks section, encryption involves:
- Splitting the encrypted value into 16-bit chunks.
- Applying the user’s EK to encrypt each chunk individually as a Twisted ElGamal ciphertext.
Similarly, decryption involves:
- Applying the user’s DK to decrypt each chunk.
- Solving a discrete logarithm (DL) problem for each chunk to recover the original values.
- Combining the recovered values to reconstruct the total amount.
Confidential store
Section titled “Confidential store”To use confidential balances, a user must first register a ConfidentialStore for that asset type (e.g., APT, USDC).
Registration requires generating a standalone keypair:
- An encryption key (EK) — stored on-chain in the
ConfidentialStore; used by others to encrypt amounts for this user. - A decryption key (DK) — kept securely by the user; used to decrypt balances and generate spend proofs.
The ConfidentialStore is instantiated per (user, asset_type) pair and managed by the confidential_asset module.
By this point, all of its fields should be familiar:
enum ConfidentialStore has key { V1 { pause_incoming: bool, normalized: bool, transfers_received: u64, pending_balance: CompressedBalance<Pending>, available_balance: CompressedBalance<Available>, ek: CompressedRistretto, auditor_hint: Option<EffectiveAuditorHint>, }}The transfers_received field counts the amount of incoming transfers so as to enforce rollovers when necessary.
The pause_incoming field allows a user to pause receiving payments, which in turn allows that user to rotate their EK/DK keypair.
The auditor_hint field is discussed later on.
Architecture
Section titled “Architecture”The diagram below shows the relationship between Confidential Asset modules:
graph TD
user["User / dapp"] -->|"register, deposit,<br/>transfer, withdraw,<br/>rollover, normalize,<br/>rotate key"| ca["aptos_framework::<br/>confidential_asset"]
ca -->|"encrypted<br/>balances"| cb["aptos_framework::<br/>confidential_balance<br/>(Pending / Available)"]
ca -->|"encrypted transfer<br/>amounts"| amt["aptos_framework::<br/>confidential_amount"]
ca -->|"verifies range proofs"| rp["aptos_framework::<br/>confidential_range_proofs"]
ca -->|"verifies Σ-protocol<br/>proofs"| sp["aptos_framework::<br/>sigma_protocol_*<br/>(registration, withdraw,<br/>transfer, key_rotation)"]
cb --> rist
amt --> rist
rp --> bp["aptos_std::<br/>ristretto255_bulletproofs"]
sp --> rist["aptos_std::<br/>ristretto255"]
bp --> rist
classDef framework fill:#fff7e6,stroke:#d99a00,color:#000;
classDef stdlib fill:#e8f4ff,stroke:#1f6feb,color:#000;
classDef user fill:#f0fff4,stroke:#2da44e,color:#000;
class ca,cb,amt,rp,sp framework
class rist,bp stdlib
class user user
Users interact with the confidential_asset module to perform every protocol operation. That module
delegates to:
confidential_balance— a single module that represents bothPendingandAvailableencrypted balances via phantom-typedCompressedBalance<T>/Balance<T>.confidential_amount— encrypted transfer-amount ciphertexts (sender, recipient, effective auditor, and any voluntary auditors).confidential_range_proofs— batched Bulletproofs verification for the new-balance and amount range proofs.sigma_protocol_*— a family of -protocol modules (sigma_protocol_proof,sigma_protocol_registration,sigma_protocol_withdraw,sigma_protocol_transfer,sigma_protocol_key_rotation, plus shared helpers insigma_protocol_utils) that verify knowledge / consistency proofs accompanying each entry function.
Under the hood, all elliptic curve cryptography is based on Ristretto255 and is implemented on top of the aptos_std::ristretto255
and aptos_std::ristretto255_bulletproofs Move modules.
Entry functions
Section titled “Entry functions”Register
Section titled “Register”public entry fun register_raw( sender: &signer, asset_type: Object<fungible_asset::Metadata>, ek: vector<u8>, sigma_proto_comm: vector<vector<u8>>, sigma_proto_resp: vector<vector<u8>>)Users must register a ConfidentialStore for each asset type they intend to transact with.
As part of this process, users generate a keypair (EK and DK) on their end and submit
a -protocol proof of knowledge of the DK corresponding to the given EK.
When a ConfidentialStore is first registered, the confidential balance is set to zero for both the pending_balance and available_balance.
This is done by the contract encrypting the value zero and storing it.
On mainnet and testnet, the registered asset type must first be allow-listed for confidential transfers by Aptos governance. Currently, only APT is allow-listed.
Lastly, registration is rejected during an emergency pause.
Deposit
Section titled “Deposit”public entry fun deposit( depositor: &signer, asset_type: Object<fungible_asset::Metadata>, amount: u64)The deposit function brings tokens into the protocol: it converts fungible assets into confidential assets by transferring the passed amount from the user’s primary FA store to their own pending balance.
This function can only be called after the user has set up their confidential store via register.
Note that the amount in this function is publicly visible, as adding new tokens to the protocol requires a normal FA transfer.
However, balances within the protocol become obfuscated through confidential transfers, ensuring privacy in subsequent transactions.
Roll over
Section titled “Roll over”public entry fun rollover_pending_balance( sender: &signer, asset_type: Object<fungible_asset::Metadata>)public entry fun rollover_pending_balance_and_pause( sender: &signer, asset_type: Object<fungible_asset::Metadata>)The rollover_pending_balance function adds the pending balance to the available one, resetting the pending balance to zero.
Rollover is required whenever a user wants to spend their pending funds. It is also enforced when the pending balance has accumulated transfers.
Rollover works without any cryptographic proofs by leveraging properties of our homomorphic encryption scheme.
The rollover_pending_balance_and_pause variant additionally pauses incoming transfers after the rollover,
which is useful when preparing for a key rotation.
Confidentially transfer
Section titled “Confidentially transfer”public entry fun confidential_transfer_raw( sender: &signer, asset_type: Object<fungible_asset::Metadata>, to: address, new_balance_P: vector<vector<u8>>, new_balance_R: vector<vector<u8>>, new_balance_R_eff_aud: vector<vector<u8>>, amount_P: vector<vector<u8>>, amount_R_sender: vector<vector<u8>>, amount_R_recip: vector<vector<u8>>, amount_R_eff_aud: vector<vector<u8>>, ek_volun_auds: vector<vector<u8>>, amount_R_volun_auds: vector<vector<vector<u8>>>, zkrp_new_balance: vector<u8>, zkrp_amount: vector<u8>, sigma_proto_comm: vector<vector<u8>>, sigma_proto_resp: vector<vector<u8>>, memo: vector<u8>)The confidential_transfer_raw function is the most complex function in the confidential asset module.
It transfers tokens from the sender’s available balance to the recipient’s pending balance, without leaking the transferred amount.
Namely, the sender encrypts the transferred amount under the recipient’s encryption key, enabling the recipient’s confidential balance to be updated homomorphically.
The transfer amount is also encrypted under the sender’s key (for the sender’s records) and under any auditor keys.
The function requires many parameters:
- New balance ciphertexts (
new_balance_P,new_balance_R,new_balance_R_eff_aud): the sender’s updated available balance after the transfer. - Amount ciphertexts (
amount_P,amount_R_sender,amount_R_recip,amount_R_eff_aud): the transfer amount encrypted under the sender’s, recipient’s, and auditor’s keys. - Voluntary auditor keys and ciphertexts (
ek_volun_auds,amount_R_volun_auds): optional additional auditor encryption keys and amount ciphertexts. - Range proofs (
zkrp_new_balance,zkrp_amount): proving the new balance and transfer amount are non-negative and within range. - -protocol proof (
sigma_proto_comm,sigma_proto_resp): proving the correctness of the transfer.
Optionally, the sender can specify a memo (memo): an opaque byte string emitted in the Transferred event, limited to get_max_memo_bytes() (256 bytes).
The memo is stored on-chain in plaintext, so the sender may want to encrypt it client-side if it is sensitive.
Withdraw
Section titled “Withdraw”public entry fun withdraw_to_raw( sender: &signer, asset_type: Object<fungible_asset::Metadata>, to: address, amount: u64, new_balance_P: vector<vector<u8>>, new_balance_R: vector<vector<u8>>, new_balance_R_aud: vector<vector<u8>>, zkrp_new_balance: vector<u8>, sigma_proto_comm: vector<vector<u8>>, sigma_proto_resp: vector<vector<u8>>)The withdraw_to_raw function allows a user to withdraw tokens from the protocol,
transferring the passed amount from the available balance of the sender to the primary FA store of the recipient.
This function enables users to release tokens while not revealing their remaining balances.
The withdrawn amount itself is publicly visible (as a u64), but the sender’s remaining balance stays hidden.
Rotate encryption key
Section titled “Rotate encryption key”public entry fun rotate_encryption_key_raw( sender: &signer, asset_type: Object<fungible_asset::Metadata>, new_ek: vector<u8>, resume_incoming_transfers: bool, new_R: vector<vector<u8>>, sigma_proto_comm: vector<vector<u8>>, sigma_proto_resp: vector<vector<u8>>)The rotate_encryption_key_raw function modifies the user’s EK and re-encrypts the available balance -components with the new EK.
The resume_incoming_transfers parameter controls whether incoming transfers are unpaused after the rotation.
To facilitate the rotation process:
- The pending balance must first be rolled over and incoming transfers paused by calling
rollover_pending_balance_and_pause. This prevents new transfers from altering the pending balance during the key rotation. - Then the EK can be rotated using
rotate_encryption_key_raw, optionally resuming incoming transfers.
Normalize
Section titled “Normalize”public entry fun normalize_raw( sender: &signer, asset_type: Object<fungible_asset::Metadata>, new_balance_P: vector<vector<u8>>, new_balance_R: vector<vector<u8>>, new_balance_R_aud: vector<vector<u8>>, zkrp_new_balance: vector<u8>, sigma_proto_comm: vector<vector<u8>>, sigma_proto_resp: vector<vector<u8>>)The normalize_raw function ensures that the available balance is reduced to 16-bit chunks for efficient decryption.
This is necessary only before the rollover_pending_balance operation, which requires the available balance to be normalized beforehand.
All other functions, such as withdraw_to_raw or confidential_transfer_raw, handle normalization implicitly, making manual normalization unnecessary in those cases.
(Un)pause incoming transfers
Section titled “(Un)pause incoming transfers”public entry fun set_incoming_transfers_paused( owner: &signer, asset_type: Object<fungible_asset::Metadata>, paused: bool)The set_incoming_transfers_paused function allows a user to pause or unpause incoming confidential transfers.
When paused, other users cannot transfer tokens to this user’s pending balance.
This is primarily used during key rotation to ensure the pending balance remains empty while the rotation is in progress.
Governance
Section titled “Governance”The following protocol-wide settings are controlled by Aptos governance (via an aptos_framework signer):
| Setting | Effect |
|---|---|
| Global auditor | Mandatory auditor for all assets without an asset-specific override |
| Asset-specific auditor | Per-token mandatory auditor; overrides the global auditor |
| Allow list | Only allow-listed FA types can use CA; withdrawals always permitted |
| Emergency pause | Halts all user operations |
Details for each setting are in the sections below.
Auditors
Section titled “Auditors”There are three types of auditors:
- Global auditor — set by governance, applies to all asset types unless overridden 👇
- Asset-specific auditor — set by governance per asset type. Takes precedence over the global auditor for that asset only.
- Voluntary auditors — extra auditors, voluntarily-specified by the sender at transfer time.
Since an asset-specific auditor supersedes the global auditor, it is useful to talk about the effective auditor for an asset type: i.e., the asset-specific auditor, if set, or the global auditor, if set, or, nothing.
Auditors have their own Twisted ElGamal keypair and have the power to:
- decrypt transfer amounts from confidential transfers, which additionally encrypt the transferred amount for the effective auditor (and for the voluntary auditors)
- decrypt available balances of any user (does not apply to voluntary auditors, who can only see the transferred amount)
The auditor configuration is wrapped in AuditorConfig, which bundles the EK with an epoch counter that
increments every time the auditor is installed or rotated (it does not increment when the auditor is removed):
enum AuditorConfig has store, drop, copy { V1 { ek: Option<CompressedRistretto>, epoch: u64, }}
enum EffectiveAuditorConfig has store, drop, copy { V1 { is_global: bool, config: AuditorConfig }}
enum EffectiveAuditorHint has store, drop, copy { V1 { is_global: bool, epoch: u64 }}Each ConfidentialStore records an EffectiveAuditorHint alongside its available_balance, so an auditor
can compare (is_global, epoch) against the current effective auditor config and tell whether their copy
of a balance ciphertext is stale.
Allow listing
Section titled “Allow listing”On mainnet and testnet, the protocol enforces an asset-type allow list: only asset types explicitly allow-listed by governance can be registered, deposited, transferred, or rolled over confidentially. Withdrawals are always permitted — even when an asset is removed from the allow list — so users can recover funds.
Allow-listing and per-asset configuration are managed by governance via the following (non-entry) functions:
set_allow_listing
Section titled “set_allow_listing”Enables or disables the allow list globally.
public fun set_allow_listing( aptos_framework: &signer, enabled: bool)set_confidentiality_for_asset_type
Section titled “set_confidentiality_for_asset_type”Enables or disables confidential transfers for a specific asset type. Only callable when the allow list is enabled; aborts with E_ALLOW_LISTING_IS_DISABLED otherwise.
public fun set_confidentiality_for_asset_type( aptos_framework: &signer, asset_type: Object<fungible_asset::Metadata>, allowed: bool)set_confidentiality_for_apt
Section titled “set_confidentiality_for_apt”Convenience wrapper that calls set_confidentiality_for_asset_type for the APT token.
public fun set_confidentiality_for_apt( aptos_framework: &signer, allowed: bool)Emergency pause
Section titled “Emergency pause”Governance can pause all user-facing operations (register, deposit, withdraw, confidential_transfer,
normalize, rollover_pending_balance, rotate_encryption_key, set_incoming_transfers_paused) via:
public fun set_emergency_paused(aptos_framework: &signer, paused: bool)While paused, every user operation aborts with E_EMERGENCY_PAUSED. Use the
is_emergency_paused view function to check the current state before
constructing transactions.
View functions
Section titled “View functions”Protocol status
Section titled “Protocol status”Whether governance has engaged the emergency pause.
#[view]public fun is_emergency_paused(): boolWhether the allow list is enabled.
#[view]public fun is_allow_listing_required(): boolWhether the asset type is allow-listed for confidential use.
#[view]public fun is_confidentiality_enabled_for_asset_type( asset_type: Object<fungible_asset::Metadata>): boolConfidential store queries
Section titled “Confidential store queries”Whether a confidential store exists for (user, asset_type).
#[view]public fun has_confidential_store( user: address, asset_type: Object<fungible_asset::Metadata>): boolUser’s current encryption key for an asset type.
#[view]public fun get_encryption_key( user: address, asset_type: Object<fungible_asset::Metadata>): CompressedRistrettoWhether the available balance is in 16-bit normal form.
#[view]public fun is_normalized( user: address, asset_type: Object<fungible_asset::Metadata>): boolWhether incoming transfers are paused.
#[view]public fun incoming_transfers_paused( user: address, asset_type: Object<fungible_asset::Metadata>): boolNumber of pending transfers since last rollover.
#[view]public fun get_num_transfers_received( user: address, asset_type: Object<fungible_asset::Metadata>): u64Balance queries
Section titled “Balance queries”Encrypted pending balance.
#[view]public fun get_pending_balance( owner: address, asset_type: Object<fungible_asset::Metadata>): CompressedBalance<Pending>Encrypted available balance.
#[view]public fun get_available_balance( owner: address, asset_type: Object<fungible_asset::Metadata>): CompressedBalance<Available>Total tokens currently locked in all confidential stores for this asset type.
#[view]public fun get_total_confidential_supply( asset_type: Object<fungible_asset::Metadata>): u64Auditor queries
Section titled “Auditor queries”Which auditor (global/asset-specific) and epoch the user’s balance ciphertext is encrypted for.
#[view]public fun get_effective_auditor_hint( user: address, asset_type: Object<fungible_asset::Metadata>): Option<EffectiveAuditorHint>The effective auditor config (asset-specific if set, otherwise global).
#[view]public fun get_effective_auditor_config( asset_type: Object<fungible_asset::Metadata>): EffectiveAuditorConfigConstants
Section titled “Constants”Maximum pending transfers before rollover is enforced (65,536).
#[view]public fun get_max_transfers_before_rollover(): u64Maximum memo length in bytes (256).
#[view]public fun get_max_memo_bytes(): u64Limitations
Section titled “Limitations”- Dispatchable FAs not supported. Only non-dispatchable Fungible Assets can be used with CA. FAs with custom
withdraw/deposit/balance/supplydispatch functions are rejected at every operation (registration, deposit, withdrawal, and transfer). - Addresses are public. Sender and recipient addresses appear in plaintext in every transaction. Only the transferred amounts and balances are hidden.
- Total supply is public. The total number of tokens currently locked across all confidential stores for an asset type is queryable via
get_total_confidential_supply.