Skip to main content
Version: v5.0.0-nightly.20260305

AIP-4626: Tokenized Vault

Source (extends the AIP-20 token contract)

AIP-4626 extends AIP-20 to describe a tokenized vault: a contract that holds an underlying asset and issues shares representing a proportional claim on that asset. It mirrors the design of ERC-4626 but adapts the share conversion arithmetic for Aztec's u128 integer type.

Share conversion

The vault tracks the total supply of shares and a vault_offset that prevents inflation attacks on the initial deposit. The conversion functions use integer arithmetic with configurable rounding direction:

#[internal("public")]
fn _convert_to_shares(assets: u128, total_assets: u128, rounding: bool) -> u128 {
let mul_term =
assets * (self.storage.total_supply.read() + self.storage.vault_offset.read());
let denominator = (total_assets + 1);
let mut shares = mul_term / denominator;
if (rounding == ROUND_UP) & (mul_term % denominator > 0) {
shares = shares + 1;
}
shares
}

#[internal("public")]
fn _convert_to_assets(shares: u128, total_assets: u128, rounding: bool) -> u128 {
let mul_term = shares * (total_assets + 1);
let denominator = (self.storage.total_supply.read() + self.storage.vault_offset.read());
let mut assets = mul_term / denominator;
if (rounding == ROUND_UP) & (mul_term % denominator > 0) {
assets = assets + 1;
}
assets
}

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 transfers assets from the caller to the vault, computes the shares due, and mints them to the recipient:

#[external("public")]
fn deposit_public_to_public(from: AztecAddress, to: AztecAddress, assets: u128, _nonce: Field) {
self.internal._validate_from_public(from);

let total_assets = self.internal._total_assets();
let shares = self.internal._convert_to_shares(assets, total_assets, ROUND_DOWN);

// Transfer assets from sender to vault
self.call(Token::at(self.storage.asset.read()).transfer_public_to_public(
from, self.address, assets, _nonce,
));

// Mint shares to the recipient
self.internal._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). Each variant transfers assets using the corresponding AIP-20 transfer function and then mints shares into the chosen output context.