> For the complete documentation index, see [llms.txt](/llms.txt)

# AIP-4626: Tokenized Vault

[Source](https://github.com/AztecProtocol/aztec-standards/tree/v6.0.0/src/vault_contract) (uses AIP-20 tokens for the asset and the shares)

AIP-4626 describes a tokenized vault: a contract that holds an underlying asset and issues shares representing a proportional claim on that asset. In the `aztec-standards` implementation, the vault is a standalone contract and the shares are a separate [AIP-20](/developers/docs/aztec-nr/standards/aip-20.md) token whose minter is the vault. The design is inspired by ERC-4626, but the separate share token makes it closer to ERC-7575, and it adapts the share conversion arithmetic for Aztec's `u128` integer type.

warning

The `aztec-standards` repository marks the vault as not production-ready. Its ordering-based reentrancy protection does not hold when the asset token or the share token has an ARC-403 authorization hook configured, and its conversions stop working near a `u128` capacity ceiling. Read the [vault README](https://github.com/AztecProtocol/aztec-standards/blob/v6.0.0/src/vault_contract/README.md) before wrapping a token.

## Share conversion

The vault reads the total supply from the share token and keeps a `vault_offset` that prevents inflation attacks on the initial deposit. The conversion functions take both as arguments, use integer arithmetic with a configurable rounding direction, and compute the intermediate product in 256 bits so it does not overflow `u128`:

```rust
/// @notice Converts an amount of assets to shares using the given exchange rate parameters
/// @param assets The amount of assets to convert
/// @param total_assets The total amount of assets in the vault
/// @param total_supply The total supply of shares
/// @param vault_offset The vault offset
/// @param rounding The rounding direction (ROUND_UP or ROUND_DOWN)
/// @return The equivalent amount of shares
#[contract_library_method]
fn _convert_to_shares(
    assets: u128,
    total_assets: u128,
    total_supply: u128,
    vault_offset: u128,
    rounding: bool,
) -> u128 {
    // `assets * (total_supply + vault_offset)` overflows u128 for large inputs; compute it in a
    // 256-bit intermediate. Rounding is unchanged (see `conversion::mul_div`).
    crate::conversion::mul_div(
        assets,
        total_supply + vault_offset,
        total_assets + 1,
        rounding == ROUND_UP,
    )
}

/// @notice Converts an amount of shares to assets using the given exchange rate parameters
/// @param shares The amount of shares to convert
/// @param total_assets The total amount of assets in the vault
/// @param total_supply The total supply of shares
/// @param vault_offset The vault offset
/// @param rounding The rounding direction (ROUND_UP or ROUND_DOWN)
/// @return The equivalent amount of assets
#[contract_library_method]
fn _convert_to_assets(
    shares: u128,
    total_assets: u128,
    total_supply: u128,
    vault_offset: u128,
    rounding: bool,
) -> u128 {
    // `shares * (total_assets + 1)` overflows u128 for large inputs; compute it in a 256-bit
    // intermediate. Rounding is unchanged (see `conversion::mul_div`).
    crate::conversion::mul_div(
        shares,
        total_assets + 1,
        total_supply + vault_offset,
        rounding == ROUND_UP,
    )
}
```

The `+ 1` in the denominator and the `vault_offset` together implement the "virtual shares" technique that prevents the first depositor from manipulating the exchange rate for subsequent depositors. Without this protection, an attacker could deposit 1 wei, then donate a large amount of the underlying asset directly to the vault, inflating the share price so that the next depositor's deposit rounds down to zero shares.

Deposits round shares down (in favor of the vault), while redemptions round assets down (also in favor of the vault). This is consistent with ERC-4626 rounding conventions and prevents rounding-based extraction attacks.

## Deposit flow

A public-to-public deposit computes the shares due, takes the assets from `from`, and has the share token mint the shares to the recipient:

```rust
#[external("public")]
fn deposit_public_to_public(from: AztecAddress, to: AztecAddress, assets: u128, nonce: Field) {
    let asset_token = self.storage.asset.read();
    let shares_token = self.storage.shares.read();
    self.internal._validate_from_public(from);

    // Calculate shares to mint
    let total_assets = self.internal._total_assets();
    let total_supply = self.internal._total_supply_of_shares();
    let vault_offset = self.storage.vault_offset.read();
    let shares =
        _convert_to_shares(assets, total_assets, total_supply, vault_offset, ROUND_DOWN);
    assert(shares > 0, "Zero shares, insufficient assets");

    // Order matters: transfer before minting to order effects safely (necessary but NOT sufficient against a hooked token — see the ARC-403 reentrancy note at the top of this contract).
    // Take the assets from the sender
    self.call(Token::at(asset_token).transfer_public_to_public(
        from,
        self.address,
        assets,
        nonce,
    ));

    // Mint shares to the recipient
    self.call(Token::at(shares_token).mint_to_public(to, shares));
}
```

The vault exposes similar entry points for the other combinations of private and public contexts (`deposit_private_to_public`, `deposit_public_to_private`, `deposit_private_to_private`). The variants that mint shares into a private balance take the number of shares as an argument and check in public that `assets` covers it, since the exchange rate is public state.
