ARC-420: VanillaToken

ARC-420 (aka Vanilla Token)

Aztec Request for Comment - VanillaToken

Abstract

A standard interface for tokens in the Aztec environment, enabling wallet providers and applications to interact with token contracts regardless of their bytecode implementation details.

ARC-420 was chosen to differentiate itself from both ERC20 and ARC20 (Bitcoin Atomicals standard), and present as a first iteration of a similar proposition standard.

Motivation

The standardization of token interfaces in the Aztec ecosystem serves two critical purposes: enabling wallet integration and facilitating DeFi protocol composability.

Wallet Integration:

  • Generic public functionality can be implemented without token-specific knowledge through standard interface methods like balance_of_public, metadata getters, and total_supply
  • Private operations require wallets to register the token’s specific bytecode (made available through block explorers or similar tools) to execute methods like balance_of_private and private transfers

DeFi Composability:

The specification enables the complete lifecycle of DeFi interactions:

  1. Private-to-Private: Core confidential transfer functionality
  2. Private-to-Public: Opening positions in DeFi protocols
  3. Public-to-Private: Transferring tokens into private state
  4. Public-to-Public: Standard public transfers

All Aztec interactions begin in private state, where users may initiate flows by moving tokens public to interact with DeFi protocols, that can later shield yields or received funds back to the private state. This enables natural interaction patterns like privately providing liquidity to public AMMs, executing swaps, and privately receiving trading fees or farming rewards.

Definitions

  • The transfer method included in Aztec packages’ repository was deprecated because of being non-explicit, the added gates for including the from: this and nonce: 0 for replicating the method with transfer_private_to_private adds roughly 1k gates, which can be negligible on the whole transfer mechanism proof size (close to 300k, when counting account contract + token proofs).
  • Similar to EVM ERC20 tokens, all “transfer flow” methods should revert if the method isn’t being called with a valid authentication mechanism, these are (1) being the msg_sender of the call, (2) presenting a valid authentication witness, or (3) other mechanisms. Instead of implementing an “approve and transfer” mechanism relying on storage, the proposed preferred mechanism is signed Authentication Witnesses, which benefit from the nonce argument to increase the security of the mechanism.
  • The user should be abstracted from knowing the underlying private Notes that represent their balance, instead, methods such as balance_of_private should aggregate them in a numeric balance, and transfer flows should make sure a user can spend multiple Notes when trying to spend an amount of Tokens.

Specification

Optional methods

  • Metadata Public Getters

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

Required methods

  • Private Getters [Brillig code]

    /// @notice Returns the total balance of Notes owned by the provided address
    //          within the calling PXE's database
    /// @dev    Raw signature: balance_of_private((Field))
    /// @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
    /// @dev    Raw signature: balance_of_public((Field))
    /// @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
    /// @dev    Raw signature: transfer_private_to_private((Field),(Field),(Field,Field),Field)
    /// @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
    /// @dev    Raw signature: transfer_private_to_public((Field),(Field),(Field,Field),Field)
    /// @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 public balance to private balance
    /// @dev    Raw signature: transfer_public_to_private((Field),(Field),(Field,Field),Field)
    /// @dev    This is a private entrypoint since the execution flow is private->public
    /// @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_private(
        from: AztecAddress,
        to: AztecAddress,
        amount: U128,
        nonce: Field
    )
    
    
    /// @notice Prepares the first step of a public to private transfer
    //          by sending a hiding point from private to public storage
    /// @dev    Raw signature: prepare_transfer_public_to_private((Field),(Field))
    /// @param  from The sender's address
    /// @param  to The recipient's address
    /// @return The hiding point that will be stored in public storage
    fn prepare_transfer_public_to_private(
        from: AztecAddress,
        to: AztecAddress
    ) -> Field
    
    
  • Public Methods

    /// @notice Transfers tokens publicly from one address to another
    /// @dev     Raw signature: transfer_public_to_public((Field),(Field),(Field,Field),Field)
    /// @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 to private balance using a previously stored
    ///         hiding point that was created in a private context
    /// @dev    Raw signature: finalize_transfer_public_to_private((Field),Field,(Field,Field),Field)
    /// @param  from The sender's address
    /// @param  to_hiding_point The previously stored hiding point from the prepare step
    /// @param  amount The amount of tokens to transfer
    /// @param  nonce A unique nonce for this transfer
    fn finalize_transfer_public_to_private(
        from: AztecAddress,
        to_hiding_point: Point,
        amount: U128,
        nonce: Field
    )
    
    

Future improvements (opinionated)

In order for higher priority to lower priority of defining. Since at the writing of this document the stage of Aztec ecosystem is pre-beta, we consider optimizations and internal mechanisms of the token to be of lesser priority, focusing our efforts in defining the ones that have the higher impact on dev experience.

  • (a) Namings

    This item stands as the top of the priorities list, as changing it needs to be replicated across all repositories that integrate with the ARC-20 token standards. Currently repositories are reading Aztec packages’ token, which doesn’t fill the specs described in this comment.

    The method names were chosen to keep a high level of explicitness in what are they’re doing. A counterpoint for them, is that they end up being too long to write them (specially in the case of Transfer to public and prepare refund), we could choose to reduce private to priv and public to pub.

    • Transfer flows:
      • transfer_private_to_public vs transfer_priv_to_pub
    • Partial notes:
      • prepare_transfer_public_to_private vs prepare_transfer_public_to_hiding_point vs transfer_hiding_point_to_public
      • finalize_transfer_public_to_private vs transfer_public_to_private_point
  • (b) Public Authentication

    While authwits show a clear benefit on the private context, their public implementation relies on storage writing instead of pop capsules in the executor’s PXE. This shows a disadvantage vs EVM’s “approve and transfer” mechanism: auth-witnesses require a fixed amount of tokens to be validated, vs approvals that may allow (i.e.) an infinite amount to be transferred on behalf of the approver.

  • (c) Unconstrained logs:
    Transferring tokens without constraining the log encryption may come handy to improve the proving time, but this should be restricted to non-contrarian setups, in which user A won’t grief user B. To implement this, each Private Method should also include a _unconstrained_logs variant that allows this behaviour.
    For example, there should exist both transfer_private_to_private and transfer_private_to_private_unconstrained_logs. The name of the variant should be reconsidered in order to warn about the effects of using these methods.

  • (d) Transfer to public and prepare refund:
    The AMM example shows the need to transfer an initially unknown amount of tokens from private to public. This is currently implemented by sequentially calling (private) transfer_private_to_public, (private) prepare_transfer_public_to_private, and then (in public) finalize_transfer_public_to_private (for the extra amount initially transferred).

    While the latter call cannot be avoided, as it needs to be executed from the public context to define the “extra amount” to return, this flow could be reduced in the private context by adding a transfer_private_to_public_and_prepare_transfer_public_to_private (see Namings) method, and equally use finalize_transfer_public_to_private. Although this is considered to be an optimization, as the three-call mechanism already enables this flow.

  • (e) Use of other Numeric Types (e.g. U253)
    If using 18 decimals (EVM convention), we won’t be able to multiply token amounts and/or process accrueing yield / interests correctly. We’re keeping U128 for now as it is enshrined in aztec-nr, but pointing out that we might need to break 1:1 decimals on bridging, or increase the numeric type to higher values to allow 18 decimals arithmetics. The decision on using now U128 is based exclusively on the nativeness (to Aztec Noir language) of the Numeric Type.

    This decision could be postponed until further development on Numeric Types and definition on how these are going to be packed into Aztec data slots.

  • (f) Public Event logs
    Public transfers could benefit from emitting a standarized event log to facilitate indexers job, while it may incur in extended DA costs with such requirement. For now, until further definitions on how public events will impact on DA, and the need of indexing such transfers, we consider this aspect of the token standard dispensable, from the point of view of the developer experience at this particular stage.

  • (g) One vs N-Notes per transfer flow

    This topic is intrinsic to each token implementation: should transfer flows “merge notes and transfer a balance”, or they should “spend a note” on each transfer flow. Should the latter be chosen, which would make the most “efficient” transfer flow (since it wouldn’t require to recursively fetch notes from the PXE), a “merging notes” method should be implemented. The current preference is to merge on each transfer flow, prioritizing the user’s “private balance” versus the user’s Notes, which the user should be abstracted from knowing they exist.

  • (h) Private metadata getters
    Add private getters for metadata (name, symbol, decimals). Considering that these could be in a future automatically generated from the Storage struct, we consider these getters to be of very low priority.

2 Likes