# Request for Comments: AIP-20: Aztec Token Standard

**URL:** <https://forum.aztec.network/t/request-for-comments-aip-20-aztec-token-standard/7737>\
**Category:** Aztec\
**Created:** [April 22, 2025, 1:15pm UTC](https://forum.aztec.network/t/request-for-comments-aip-20-aztec-token-standard/7737 "2025-04-22T13:15:51Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![wonderland](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/wonderland/32/2437_2.png) [@wonderland](https://forum.aztec.network/u/wonderland)\
**Post date:** [April 22, 2025, 1:15pm UTC](https://forum.aztec.network/t/request-for-comments-aip-20-aztec-token-standard/7737/1 "2025-04-22T13:15:51Z")

</div>

## 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._

```rust
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](https://wonderland.xyz/), describing the design decisions taken from the one in [Aztec Packages repository](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr). 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

```rust
fn get_name() -> FieldCompressedString
fn get_symbol() -> FieldCompressedString
fn get_decimals() -> u8

```

### Required Methods

### Private Getters [Utility Methods]

```rust
/// @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

```rust
/// @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

```rust
/// @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

```rust
/// @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:**

```rust
/// @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:**

```rust
/// @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:**

```rust
/// @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:**

```rust
/// @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: Comprehensive collection of reusable, standardized contracts for the Aztec Network](http://github.com/defi-wonderland/aztec-standards/) repository.

- **Note Structure**

- **Fetching and Processing Mechanism**

- **Commitment Mechanism**

- **Log Encryption**

- **Supply Management**

- **Authentication Witnesses**  
The presence of a `nonce` 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](https://github.com/defi-wonderland/aztec-standards/tree/dev/src/token_contract) 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.

---

<div class="post-metadata">

**Author:** ![francoperez.03](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/francoperez.03/32/2538_2.png) [@francoperez.03](https://forum.aztec.network/u/francoperez.03)\
**Post date:** [May 20, 2025, 9:20pm UTC](https://forum.aztec.network/t/request-for-comments-aip-20-aztec-token-standard/7737/2 "2025-05-20T21:20:07Z")

</div>

## When is it better to use a **2-step “public → private” flow** instead of a single private transfer?

Hi everyone

I’m experimenting with this standard — which I find incredibly powerful and well-designed 🔥— and I want to congratulate you for their work 👏. While exploring it, I came across two ways of moving funds from a **public balance** to a **private note** :

| Flow | Who signs | Rough steps |
| --- | --- | --- |
| **Single private tx** | _Only the sender_ | `transfer_public_to_private(sender, receiver, amount, nonce)` — executed as a **private** function; the note is created directly in the sender’s PXE. |
| **Two-step pattern** | 1️⃣ _Receiver_ (private)  
2️⃣ _Sender_ (public) | 1. `initialize_transfer_commitment(from, to)`  
2. `transfer_public_to_commitment(sender, amount, commitment)` |

I saw that the people prefers the **two-step pattern** , but I’d love to better understand the trade-offs before choosing a design.

* * *

### What I currently understand

- A public function **can’t directly create a private note for someone else** — that’s why the _commitment trick_ exists.
- In a private function, the sender can **create an encrypted note** for the receiver locally using their PXE, without the receiver being online.
- The entire private execution happens client-side in the PXE, and **only the final proof is submitted to the sequencer**.

* * *

### What I’m still unclear about

1. Is there **any security risk** in allowing the sender to create a private note for the receiver in a single private transaction ?
2. If there’s an AMM contract, and Alice wants to deposit her _private tokens_ into the AMM’s _public state_, the AMM would need to call the token contract with Alice’s AuthWit.  
Couldn’t these two chained calls, triggered by Alice, be executed entirely inside her PXE and then committed as one transaction? Why wouldn’t that work?

* * *

This feels like my key question, and I’d be really happy to understand how this works in practice, especially in cases where chaining private logic like this seems intuitive but might break Aztec’s model.

I find the `transfer_private_to_public_with_commitment` function particularly clever! since it prepares the return path in the same step as the deposit 🚀

Would truly appreciate any insights or real-world patterns from folks building with Aztec 🙏

Thanks in advance!

– Franco –

---

<div class="post-metadata">

**Author:** ![wei3erHase](https://dub1.discourse-cdn.com/flex013/user_avatar/forum.aztec.network/wei3erhase/32/2263_2.png) [@wei3erHase](https://forum.aztec.network/u/wei3erHase)\
**Post date:** [May 20, 2025, 9:45pm UTC](https://forum.aztec.network/t/request-for-comments-aip-20-aztec-token-standard/7737/3 "2025-05-20T21:45:40Z")

</div>

Ey Franco, thanks for your reply, most of the design was taken from Aztec’s Token, meant for testing, but revealed most of the issues we plan here to address. In this case, the main difference between these 2 methods is syncronicity.

As you very well remark, `transfer_public_to_private` is a private tx, this means that the User, within the private context of the execution, instantly has disponibility of the funds he’s transferring from public to private, only that the reduction of public balance happens afterwards, in the public context of the execution.

On the other side, the `initialize_transfer_commitment + transfer_public_to_commitment` pattern happens (at least the 2nd call) on the public scope of execution, this means that if the User wants to use the funds that are sent to his private balance, he will have to perform another transaction afterwards.

These are meant for different purposes:

- transfer public to private: shield a fixed amount of tokens to later spend them privately
- initialize commitment + transfer to commitment: shield an unknown amount of tokens, that will be defined on the public context of the execution, for example, in the AMM case (where amount out is defined by the AMM’s liquidity)

Remember that the `commitment + partial note finalization` pattern is quite native to Aztec Noir, making it possible to create partial notes of any kind (not only UintNotes), to be partially completed later on from the public context (making the initialized part private to all).
