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.
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
/// @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
/// @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
) -> PartialUintNote
/// @notice Transfers tokens from private balance to a commitment
/// @param from The sender's address
/// @param amount The amount of tokens to transfer
/// @param commitment The partial note representing the commitment
/// @param nonce A unique nonce for this transfer
fn transfer_private_to_commitment(
from: AztecAddress,
amount: u128,
commitment: PartialUintNote,
nonce: Field
)
/// @notice Initializes a transfer commitment to be used for transfers/mints
/// @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
) -> PartialUintNote
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
/// @param from The sender's address
/// @param amount The amount of tokens to transfer
/// @param commitment The partial note representing the commitment
/// @param nonce A unique nonce for this transfer
fn transfer_public_to_commitment(
from: AztecAddress,
amount: u128,
commitment: PartialUintNote,
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
/// @param amount The amount of tokens to mint
/// @param commitment The partial note representing the commitment
fn mint_to_commitment(
amount: u128,
commitment: PartialUintNote
)
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
PartialUintNote
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 other contracts that want to implement their own logic.