Required reading: this proposal builds on top of AIP-20: Aztec Token Standard leveraging the multi-constructor setup.
Introduction
The Tokenized Vault (TV) standard is an adaptation of the ERC-4626, designed to provide a unified interface for yield distribution within the Aztec network. Users can deposit an underlying asset into the vault and receive proportional shares. Any yield accrued by the vault automatically increases the value of each share, ensuring fair, low-cost reward distribution.
While the TV contract publicly holds the underlying token deposits and accrued yield, shares can be held either publicly or privately. Likewise, underlying assets can be deposited from or withdrawn to both public and private balances. All vault methods are fully compliant with the vanilla Aztec token model described in AIP-20 and support any combination of public/private execution and asset/share accounting.
Problem Statement and Context
DeFi protocols increasingly rely on vaults to aggregate assets and distribute yield. However, there’s a need for a gas-efficient solution with privacy guarantees that simplifies integration with other applications.
This Tokenized Vault standard fills that gap by:
- Standardizing deposit, issue (a.k.a. mint), withdraw, and redeem across privacy variants.
- Allowing users to keep share balances private when desired.
- Minimizing on-chain computations and reducing complexity to lower gas costs.
- Providing an easy-to-use and integrable solution for dApp developers within the ecosystem.
Specification
One Standard
The vault functionality is integrated directly into the vanilla token and gets enabled only if the contract is initialized via a new constructor constructor_with_asset
. This would add a new underlying token (asset) to the storage structure and expose the vault methods, simplifying code maintenance and reducing duplicated logic. In the future, an inheritance scheme could allow this functionality to reside in a separate contract.
Added Functions
The core operations follow the same patterns and methods as ERC-4626 for deposit, issue, withdraw, and redeem, accommodating all privacy variants.
Since share-to-asset rates depend on the total share supply and the contract’s public asset balance, all the vault’s methods, including private ones, require public state validation. This adds the challenge of handling rate changes between private and public execution. To handle this, some private methods will require the user to specify the maximum, minimum or exact amounts of shares or assets they find acceptable to send or receive.
Additionally, some private methods will be implemented in two variations:
- Exact Variant: Returns any excess shares or assets sent to the contract for deposit and redeem methods. Completes the transfer of outstanding shares or assets from the contract to the recipient for issue and withdraw methods.
- Cheaper Variant: Simply retains the change, offering a more gas-efficient option if slippage was not the main concern.
This dual approach addresses scenarios where the asset/share rate may vary between private and public execution. Depending on the case, paying additional fees to retrieve the change might be economically sensible or not.
Methods Overview
A total 19 methods are proposed: 6 for deposit, 4 for issue, 5 for withdraw and 4 for redeem. Certain variations have been intentionally excluded, either because their behavior is functionally identical to an existing method or because an alternative already achieves the same outcome more efficiently and without slippage risk.
The method’s names are of the form: <Type>_<Incoming Context>_to_<Outgoing Context>()
where:
- Type: deposit, issue, withdraw or redeem
- Incoming Context: public or private. Whether the tokens (assets or shares) being sent to the TV contract come from a public or private balance.
- Outgoing Context: public or private. Whether the tokens (assets or shares) are being sent from the TV contract to the recipient’s public or private balance.
- Optional
_exact
suffix: change/outstanding tokens are reimbursed/settled during public execution.
Examples:
deposit_public_to_private
:assets
are transferred to the TV contract fromfrom
’s public balance andshares
are minted toto
’s private balance. Although this is a private function,assets
andshares
amounts are passed (i.e. leaked) into public context to validate the rate.deposit_public_to_private_exact
:assets
are transferred to the TV contract fromfrom
’s public balance,min_shares
are minted toto
’s private balance during private execution, which allows the recipient to use these shares to immediately interact with other protocols, and the rest of the sharesto
is entitled to givenassets
is calculated and minted toto
’s private balance during public execution.redeem_public_to_public
:shares
are burnt fromfrom
’s public balance and the corresponding amount of assets is calculated and sent toto
’s public balance.
The following diagrams illustrate the private_to_private
variations of the deposit function. Note that even though assets are sent from a private balance, they are pulled into the TV’s public balance. All assets are held publicly, since the TV contract cannot hold private balances.
Execution Flow: deposit_private_to_private
sequenceDiagram
actor User
participant TV as Tokenized Vault
participant UT as Underlying Token
User->>TV: deposit_private_to_private(from, to, assets, shares)
TV->>UT: transfer_private_to_public(from, TV, assets)
User-->>+TV: assets [UT]
TV->>TV: _validate_ratio
par Public Execution
TV-->>TV: max_shares = convert_to_shares(assets)
TV-->>TV: assert(shares <= max_shares)
end
TV->>TV: _mint_to_private(to, shares)
TV-->>User: shares [TV]
par Public Execution
TV->>TV: total_supply += shares
end
Execution Flow: deposit_private_to_private_exact
sequenceDiagram
actor User
participant TV as Tokenized Vault
participant UT as Underlying Token
User->>TV: deposit_private_to_private_exact(from, to, assets, min_shares)
TV->>UT: transfer_private_to_public(from, TV, assets)
User-->>+TV: assets [UT]
TV->>TV: initialize_transfer_commitment(from, to)
TV->>TV: _validate_ratio_and_complete_issuance
par Public Execution
TV-->>TV: shares = convert_to_shares(assets)
TV-->>TV: outstanding_shares = shares - min_shares
TV->>TV: mint_to_commitment(TV, commitment, outstanding_shares)
TV->>TV: total_supply += shares
TV-->>User: outstanding_shares [TV]
end
TV->>TV: _mint_to_private(to, min_shares)
TV-->>User: min_shares [TV]
The only difference in the deposit_public_to_private
flows is that assets are transferred using transfer_public_to_public
instead of transfer_private_to_public
.
Below is the full list of the 19 proposed methods:
/* ====================== DEPOSIT ========================= */
#[public]
fn deposit_public_to_public(from, to, assets, nonce);
#[private]
fn deposit_public_to_private(from, to, assets, shares, nonce);
#[private]
fn deposit_private_to_private(from, to, assets, shares, nonce);
#[private]
fn deposit_private_to_public(from, to, assets, nonce);
#[private]
fn deposit_public_to_private_exact(from, to, assets, min_shares, nonce);
#[private]
fn deposit_private_to_private_exact(from, to, assets, min_shares, nonce);
/* ======================= ISSUE ========================== */
#[public]
fn issue_public_to_public(from, to, shares, max_assets, nonce);
#[private]
fn issue_public_to_private(from, to, shares, max_assets, nonce);
#[private]
fn issue_private_to_public_exact(from, to, shares, max_assets, nonce);
#[private]
fn issue_private_to_private_exact(from, to, shares, max_assets, nonce);
/* ====================== WITHDRAW ======================== */
#[public]
fn withdraw_public_to_public(from, to, assets, nonce);
#[private]
fn withdraw_public_to_private(from, to, assets, nonce);
#[private]
fn withdraw_private_to_private(from, to, assets, shares, nonce);
#[private]
fn withdraw_private_to_public_exact(from, to, assets, max_shares, nonce);
#[private]
fn withdraw_private_to_private_exact(from, to, assets, max_shares, nonce);
/* ======================= REDEEM ========================= */
#[public]
fn redeem_public_to_public(from, to, shares, nonce);
#[private]
fn redeem_private_to_public(from, to, shares, nonce);
#[private]
fn redeem_private_to_private_exact(from, to, shares, min_assets, nonce);
#[private]
fn redeem_public_to_private_exact(from, to, shares, min_assets, nonce);
As mentioned, certain variations have been intentionally excluded. For example, none of the public_to_public
methods are implemented in their exact
variants, because they are already exact by default. Methods like withdraw_private_to_public
and redeem_public_to_private
are only implemented in their _exact
variants, otherwise redeem_public_to_private
and withdraw_private_to_public
, respectively, already serve the same purpose without risk of overspending shares.
Implementation
An implementation that complies with the proposed interface is actively being developed to be used by contracts that want to leverage this tokenized vault standard or to be forked for contracts that want to implement their own logic.
Technical Challenges
- Asset<>Share conversion involves multiplying two token balances and dividing by a third in public context. With
u128
balances and 18 decimals’ tokens, the multiplication part can easily overflow. To address this, two approaches are currently under evaluation for future implementation:- Precision reduction – Reduce decimals before multiplying to stay within
u128
limits. This has minimal impact on most tokens. - Big number arithmetic – Use a bignum library to safely handle large multiplications in a public context. The development of such a library is still under discussion.
- Precision reduction – Reduce decimals before multiplying to stay within
- Inflation attack protection will be implemented in a similar fashion to OpenZeppelin’s ERC4626 contract. This decision, however, is still under review.
- The proposed design could be simplified if authwits supported variable amounts or conditional authorizations in the future, removing the need to predefine exact token values for authorization.