Abstract
This standard defines a unified API for token implementations within the Aztec protocol ecosystem. It establishes essential functionality for token transfers between accounts across both private and public execution contexts.
Due to Aztec’s sequential transaction processing architecture (private execution preceding public execution), transferring tokens from public to private state requires a preliminary transfer commitment. This commitment must be initialized from the private context and subsequently processed in the public context.
Introduction
Aztec is a zk-rollup providing humanity with privacy-preserving programmable digital money, identity solutions, games, and more. The key thing here is that users of apps built on top of it can choose which information to reveal to the world, and which to keep close. As you probably know, on Ethereum today, everything is publicly visible by everyone. if you need to summarize this in one phrase, the thing is that aztec brings privacy to ethereum. Now you have private functions AND public functions, composability and messaging between those two realities. We will introduce some foundational concepts bellow.
Public vs Private execution
Aztec transactions are executed in two different environments. Private environment, which happen on user devices (using the Private Execution Environment (PXE)) to maintain privacy and public environment by a network of nodes, similar to other blockchains.
While public function calls happen exclusively in public nodes, private function calls begin in the user’s device and are then relayed into the public network. This gives us the ability to execute public functions from private but not private functions from the public environment. These calls will get “queued” from the private environment, and be executed in order of appearance.
Notes
A Note is a cryptographically committed, privately owned data object representing a discrete unit of value or state. It’s the atomic unit of private state in the protocol.
A note is:
- Committed to via a hash (note_commitment)
- Encrypted and stored on-chain in a Merkle tree (the note tree)
- Decrypted off-chain by the rightful owner using their secret viewing key
- Spent by proving knowledge of its preimage and secret key, resulting in a nullifier
Notes encapsulate both the value being transferred and the authorization logic for who can spend them.
Usually, we can think of a Note as a struct that has the following fields:
This is not exactly the structure of a Note, but an example to get the mind around the concept.
struct Note {
value: Field,
owner_pub_key: AztecAddress,
randomness: Field,
contract_address: AztecAddress.
}
And the note commitment will be similar to: note_commitment = hash(value, asset_id, owner_pub_key, randomness)
.
If Bob receives a private payment of 25 DAI, then Bob scans the blockchain for notes encrypted to his key. When he finds this one, he can decrypt it and later spend it by proving knowledge of the commitment’s preimage and his secret key.
Nullifiers
A nullifier is a unique cryptographic fingerprint derived from a spent note. It acts as a non-linkable yet verifiable proof of spend, allowing the protocol to prevent double-spending while maintaining privacy.
Each nullifier is deterministically derived from a note and the secret key of the spender, but in such a way that:
- It cannot be reverse-engineered to reveal the note or spender.
- It does not link to the new notes being created in the same transaction.
- It ensures that each note can be spent only once.
Nullifiers are inserted into a nullifier tree. A transaction is valid only if the nullifiers it generates are not already present in the tree.
Some properties are that they are:
- One-to-one: Each note has exactly one nullifier
- Non-malleable: Cannot be modified without invalidating the proof
- Unlinkable: Does not reveal which note or who spent it
- Deterministic and unique: Prevents reuse of a note
It will be computed as nullifier = H(note_secret_data || secret_key || constant)
, where note_secret_data
includes value, randomness, and asset type, the secret_key
is associated with the note’s owner, H
is collision-resistant hash function and the constant
is circuit-specific and ensures domain separation.
So, let’s say that Alice owns a note where:
value = 10
owner_pub_key = H(secret_key)
randomness = r1
When spending the note, she computes: nullifier = hash(H(10, r1, asset_id), secret_key, CONST)
. This nullifier is then inserted into the tree and any future attempt to spend the same note will result in a duplicate nullifier and cause the transaction to be rejected.
Commitment
A commitment is a Field
element that acts as a placeholder for a yet-to-be-revealed Note
. It serves as a bridge between the private and public execution contexts by allowing users to register the intent to transfer value without disclosing details like recipient or amount.
Public nodes, which cannot access private state or user secrets, use commitments to track these transfers in a verifiable but opaque way. Formally, a commitment is computed as a hash of one or more fields that would otherwise be used to construct a Note, such as:
commitment = hash(amount, recipient_public_key, randomness, asset_id, ...)
Commitments are:
- Unlinkable: their preimage is not exposed on-chain.
- One-way: cannot be reversed or interpreted without secrets.
- Verifiable: can be checked for inclusion and equality without revealing their contents.
- Hybrid between public and private: support transitions between execution contexts.
Summary
A Fungible Token implementation, proposed by Wonderland, describing the design decisions taken from the one in Aztec Packages repository. This document aims to establish the interface methods, so that all future implementations can seamlessly adapt.
Details
This Token implements the following changes to the previous implementation:
-
Method namings
-
Mintable vs Non Mintable variations
-
Note fetching mechanism
-
Partial Notes implementation
Method Categories
Private Methods:
- Transfer tokens from private state to private state
- Transfer tokens from private state to public state
- Transfer tokens from private state to commitment
- Initialize a transfer commitment
Public Methods:
- Transfer tokens from public state to public state
- Transfer tokens from public state to commitment
This standard ensures interoperability between token implementations and applications in the Aztec ecosystem while maintaining the protocol’s privacy and execution guarantees.
Note: Transferring to a commitment (either from private or public) is executed in the public space, although the recipient address is being hidden, it leaks information about the action being taken and the transferred amount.
Specification
Optional Methods
Metadata Getters
fn get_name() -> FieldCompressedString
fn get_symbol() -> FieldCompressedString
fn get_decimals() -> u8
Required Methods
Private Getters [Utility Methods]
/// @notice Returns the total balance of Notes owned by the provided address
/// within the calling PXE's database
/// @dev Balance of private can't be proven to return the correct value.
/// Users may provide the total or less than what they actually have.
/// @param from The address to check the balance for
/// @return The aggregated balance from all Notes where owner=from
fn balance_of_private(
from: AztecAddress
) -> u128
Public Getters
/// @notice Returns the total amount of tokens in circulation
/// (sum of public and private balances)
/// @return The total supply of tokens
fn total_supply() -> u128
/// @notice Returns the public balance of the provided address
/// @param from The address to check the balance for
/// @return The public balance for the specified address
fn balance_of_public(
from: AztecAddress
) -> u128
Private Methods
/// @notice Transfers tokens privately from one address to another
/// @param from The sender's address
/// @param to The recipient's address
/// @param amount The amount of tokens to transfer
/// @param nonce A unique nonce for this transfer
fn transfer_private_to_private(
from: AztecAddress,
to: AztecAddress,
amount: u128,
nonce: Field
)
/// @notice Transfers tokens from private balance to public balance
/// @param from The sender's address
/// @param to The recipient's address
/// @param amount The amount of tokens to transfer
/// @param nonce A unique nonce for this transfer
fn transfer_private_to_public(
from: AztecAddress,
to: AztecAddress,
amount: u128,
nonce: Field
)
/// @notice Transfers tokens from private balance to public balance and initializes a transfer commitment
/// @dev *Combines a transfer to public state with the emission
/// of a matching commitment, enabling proofs that link both transitions.*
/// @param from The sender's address
/// @param to The recipient's address
/// @param amount The amount of tokens to transfer
/// @param nonce A unique nonce for this transfer
/// @return The partial note representing the commitment
fn transfer_private_to_public_with_commitment(
from: AztecAddress,
to: AztecAddress,
amount: u128,
nonce: Field
) -> Field
/// @notice Transfers tokens from private balance to a commitment
/// @dev *Burns Notes in the private context and emits a commitment representing
/// the recipient's future Note. This is useful when the sender does not
/// have the recipient’s viewing key.*
/// @param from The sender's address
/// @param commitment The partial note representing the commitment
/// @param amount The amount of tokens to transfer
/// @param nonce A unique nonce for this transfer
fn transfer_private_to_commitment(
from: AztecAddress,
commitment: Field,
amount: u128,
nonce: Field
)
/// @notice Initializes a transfer commitment to be used for transfers/mints
/// @dev *Returns a computed commitment from known inputs.*
/// @param from The sender's address
/// @param to The recipient's address
/// @return The partial note representing the commitment
fn initialize_transfer_commitment(
from: AztecAddress,
to: AztecAddress
) -> Field
Public Methods
/// @notice Transfers tokens publicly from one address to another
/// @param from The sender's address
/// @param to The recipient's address
/// @param amount The amount of tokens to transfer
/// @param nonce A unique nonce for this transfer
fn transfer_public_to_public(
from: AztecAddress,
to: AztecAddress,
amount: u128,
nonce: Field
)
/// @notice Transfers tokens from public balance to a commitment
/// @dev *Reduces public balance and creates a pending private Note commitment.
/// The actual Note must be created off-chain by the recipient.*
/// @param from The sender's address
/// @param commitment The partial note representing the commitment
/// @param amount The amount of tokens to transfer
/// @param nonce A unique nonce for this transfer
fn transfer_public_to_commitment(
from: AztecAddress,
commitment: Field,
amount: u128,
nonce: Field
)
Extensions
Mintable
This section of the standard covers the minting interface, that is optionally present in tokens, depending on the constructor used to initialize the Token contract (read Supply Management below).
Private Methods:
/// @notice Mints tokens to a private balance
/// @dev Method caller should be authorized
/// @param from The message sender address (used for log encryption)
/// @param to The recipient's address
/// @param amount The amount of tokens to mint
fn mint_to_private(
from: AztecAddress,
to: AztecAddress,
amount: u128
)
Public Methods:
/// @notice Mints tokens to a public balance
/// @dev Method caller should be authorized
/// @param to The recipient's address
/// @param amount The amount of tokens to mint
fn mint_to_public(
to: AztecAddress,
amount: u128
)
/// @notice Mints tokens to a commitment
/// @dev Method caller should be authorized
/// @dev *Issues newly minted tokens into a commitment
/// rather than a full Note or public balance.
/// Typically used when minting to privacy-preserving destinations.*
/// @param commitment The partial note representing the commitment
/// @param amount The amount of tokens to mint
fn mint_to_commitment(
commitment: Field
amount: u128,
)
Burnable
This section of the standard covers the burning interface.
Private Methods:
/// @notice Burns tokens from a private balance
/// @param from The address from which tokens will be burned
/// @param amount The amount of tokens to burn
/// @param nonce A unique nonce for this operation
fn burn_private(
from: AztecAddress,
amount: u128,
nonce: Field
)
Public Methods:
/// @notice Burns tokens from a public balance
/// @param from The address from which tokens will be burned
/// @param amount The amount of tokens to burn
/// @param nonce A unique nonce for this operation
fn burn_public(
from: AztecAddress,
amount: u128,
nonce: Field
)
Opinionated Implementation
This section covers an opinionated implementation of the AzRC-20 standard, available in GitHub - defi-wonderland/aztec-standards repository.
-
Note Structure
The standard defines operations for transferring balances, where private balances are implemented through the accumulation of Notes (a native Aztec concept). The interface remains agnostic to the internal structure of these Notes. In this implementation, Notes have the following structure:
struct UintNote { owner: AztecAddress, amount: u128, random: Field }
-
Fetching and Processing Mechanism
The PXE (Private eXecution Environment) must create proofs involving a number of Notes to be spent or burned. Since the number of Notes processed isn’t publicly revealed in ZK circuits, circuits must account for the maximum possible Notes processed. To optimize proof size:
- Initial Note Limit: The implementation sets an initial limit of 2 Notes per transfer operation.
- Recursive Mechanism: If the initial Notes don’t cover the total amount to transfer/burn, a recursive mechanism fetches and processes additional Notes in batches.
- Note Consolidation: Using 2 Notes initially helps reduce the total number of Notes a user holds over time. If only 1 Note were used instead (as initial note limit), almost every transfer would result in 1 Note spent and 1 change Note created, preventing consolidation.
-
Commitment Mechanism
Like Notes, commitments can have multiple implementations while maintaining the same interface properties. This implementation leverages Aztec Noir’s
Field
package for the commitment mechanism required in public-to-private transfers. -
Log Encryption
This implementation uses AES-128 encryption for Note sharing, ensuring private information remains confidential when transmitting Notes between parties. The encryption is handled through Aztec’s
encode_and_encrypt_note
function.The encryption chosen needs to be the same that the Commitment Mechanism uses, because of fungibility between transferring to a commitment vs to a private address.
-
Supply Management
- Fixed Supply Model: Using
constructor_with_initial_supply
creates a token with a predetermined initial supply assigned to a specified address. In this case, no minter is assigned, so all mintable methods will revert when called, effectively creating a capped supply token. - Managed Supply Model: Using
constructor_with_minter
assigns minting privileges to a designated address, allowing for dynamic supply management. This model supports ongoing minting operations through the mintable extension methods.
- Fixed Supply Model: Using
-
Authentication Witnesses
The presence of anonce
parameter in many of the interface methods is specifically designed to support authentication witnesses. These witnesses serve as cryptographic proof that the owner of the Notes is indeed authorizing their spending.
This approach enables functionality similar to ERC-20’s transferFrom
while preserving the privacy guarantees of the Aztec protocol and optimizing for the ZK context.
1. Ownership Verification: When a method is called with a from
address that doesn’t match the msg_sender
, the implementation must verify an authentication witness to ensure the caller has permission to spend from that address.
2. Nonce Mechanism: The nonce
parameter prevents replay attacks by ensuring each authentication witness can only be used once. When the caller is the msg_sender
(i.e., spending their own tokens), the nonce should be 0.
The nonce
is added on the method interface (rather than popped out within the token via capsules), so that other smart-contracts can implement logic on top of the nonce (i.e., an AMM ensuring the nonce represents a “swap” rather than an “LP” approval).
3. Implementation Approach: Rather than using an “approve and transfer” pattern that relies on storage (as in ERC-20), this standard leverages authentication witnesses to authorize spending directly, reducing storage costs and maintaining privacy.
Implementation
An implementation that complies with the proposed interface was developed to be used by contracts that want to leverage this token or to be forked for contracts that want to implement their own logic.