Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/dripper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

The `Dripper` contract provides a convenient faucet mechanism for minting tokens into private or public balances. Anyone can easily invoke the functions below to request tokens for testing or development purposes.

> **Note**: This contract is designed for development and testing environments only. Do not use in production. As a dev utility rather than a standard, it is intentionally outside the repository's automated test scope.
> [!WARNING]
> The Dripper is an **uncapped, permissionless minter**: `drip_to_public` / `drip_to_private` let *anyone* mint *any* amount (up to `u64::MAX` per call, repeatable) of any token for which the Dripper is the configured `minter`. Its only safety boundary is external — it must **never be granted `minter` on a token that holds real value**, on any network. It is a development/testing faucet only, and as a dev utility rather than a standard it is intentionally outside the repository's automated test scope.

## Public Functions

Expand Down
3 changes: 3 additions & 0 deletions src/multitoken_contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ The `MultiToken` contract implements an ERC-1155-like multi-token with Aztec-spe

Compared to the single-asset [`Token`](../token_contract/README.md), every balance-changing function takes an extra `id: Field` selecting the token, there is no `decimals` and no `total_supply`, and the on-chain event is `TransferSingle` (ERC-1155 naming) instead of `Transfer`.

> [!WARNING]
> Like everything in this repository, `MultiToken` is **experimental, unaudited software** (see the repo-level [Security Status](../../README.md#️-security-status-unaudited)). One behaviour in particular is easy to misuse: a transfer commitment does **not** bind the token id or amount — the completer chooses both. This is intentional, but it means a commitment is **not a payment guarantee**. Read the [Commitment trust model](#commitment-trust-model) before using one in an escrow or marketplace flow.

## ARC-403: Authorization Hook

Like `Token`, this contract implements the optional ARC-403 authorization hook: when an `auth_contract` is configured, every transfer and burn calls it before mutating balances, and the operation reverts if the hook reverts. If `auth_contract` is the zero address, the hook is disabled and the token behaves as a plain multi-token. The interface is **id-bearing** — the hook receives the token id so policies can differ per id:
Expand Down
53 changes: 37 additions & 16 deletions src/vault_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ pub mod test;

use aztec::macros::aztec;

// ============================================================================
// ⚠️ ARC-403 reentrancy limitation — READ BEFORE RELYING ON THE ORDERING BELOW
// ----------------------------------------------------------------------------
// Throughout this contract, `// Order matters:` comments arrange asset transfers
// and share mint/burns so that any callback would observe either fully-pre- or
// fully-post-operation state. That reasoning is ONLY sound if a token transfer
// is indivisible. It is NOT when the asset or shares token has an ARC-403
// authorization hook configured: the hook runs *inside* the token transfer,
// BEFORE the balance actually moves (see token_contract `_call_auth_*`, invoked
// ahead of the balance write). A hooked token therefore hands control to the
// authorization contract while this vault is mid-operation — the exact
// intermediate state the ordering is written to prevent — and a reentrant vault
// call can read a share price no completed operation would produce.
//
// The ordering below is thus necessary but NOT sufficient: it does not protect
// a vault whose asset or shares token carries a non-zero `auth_contract`.
// Only wrap tokens with no hook, or a fully trusted one. This is a known,
// unresolved exposure (security audit 2026-08, findings F-001/F-002); see the
// Vault README warning. The per-site comments below are kept for their ordering
// intent but should be read against this limitation.
// ============================================================================
#[aztec]
pub contract Vault {
use aztec::{
Expand Down Expand Up @@ -149,7 +170,7 @@ pub contract Vault {
_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 neutralize ARC-403 reentrancy.
// 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,
Expand Down Expand Up @@ -209,7 +230,7 @@ pub contract Vault {
let shares_token = self.storage.shares.read();
_validate_from_private::<5>(self.context, from);

// Order matters: transfer before minting to neutralize ARC-403 reentrancy.
// 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_private_to_public(
from,
Expand All @@ -235,7 +256,7 @@ pub contract Vault {
let asset_token = self.storage.asset.read();
_validate_from_private::<4>(self.context, from);

// Order matters: transfer before minting to neutralize ARC-403 reentrancy.
// 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_private_to_public(
from,
Expand Down Expand Up @@ -312,7 +333,7 @@ pub contract Vault {
let partial_note =
self.call(Token::at(shares_token).initialize_transfer_commitment(to, self.address));

// Order matters: transfer before minting to neutralize ARC-403 reentrancy.
// 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_private_to_public(
from,
Expand Down Expand Up @@ -361,7 +382,7 @@ pub contract Vault {
let vault_offset = self.storage.vault_offset.read();
let assets = _convert_to_assets(shares, total_assets, total_supply, vault_offset, ROUND_UP);

// Order matters: transfer before minting to neutralize ARC-403 reentrancy.
// 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,
Expand Down Expand Up @@ -441,7 +462,7 @@ pub contract Vault {
let asset_commitment =
self.call(Token::at(asset_token).initialize_transfer_commitment(from, self.address));

// Order matters: transfer before minting to neutralize ARC-403 reentrancy.
// 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 max_assets from the sender
self.call(Token::at(asset_token).transfer_private_to_public(
from,
Expand Down Expand Up @@ -482,7 +503,7 @@ pub contract Vault {
let asset_commitment =
self.call(Token::at(asset_token).initialize_transfer_commitment(from, self.address));

// Order matters: transfer before minting to neutralize ARC-403 reentrancy.
// 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 max_assets from the sender
self.call(Token::at(asset_token).transfer_private_to_public(
from,
Expand Down Expand Up @@ -524,7 +545,7 @@ pub contract Vault {
let vault_offset = self.storage.vault_offset.read();
let shares = _convert_to_shares(assets, total_assets, total_supply, vault_offset, ROUND_UP);

// Order matters: burn before transferring to neutralize ARC-403 reentrancy.
// Order matters: burn before transferring to order effects safely (necessary but NOT sufficient against a hooked token — see the ARC-403 reentrancy note at the top of this contract).
// Burn the sender's shares on the shares token
self.call(Token::at(shares_token).burn_public(from, shares, nonce));

Expand All @@ -548,7 +569,7 @@ pub contract Vault {
let asset_token = self.storage.asset.read();
_validate_from_private::<4>(self.context, from);

// Order matters: burn after calculating shares in public to neutralize ARC-403 reentrancy.
// Order matters: burn after calculating shares in public to order effects safely (necessary but NOT sufficient against a hooked token — see the ARC-403 reentrancy note at the top of this contract).
// Calculate and burn the sender's shares
self.enqueue_self.settle_withdraw_public_to_private_internal(from, assets, nonce);

Expand Down Expand Up @@ -579,7 +600,7 @@ pub contract Vault {
// Burn shares from the sender's private balance on the shares token
self.call(Token::at(shares_token).burn_private(from, shares, nonce));

// Order matters: transfer after burning to neutralize ARC-403 reentrancy.
// Order matters: transfer after burning to order effects safely (necessary but NOT sufficient against a hooked token — see the ARC-403 reentrancy note at the top of this contract).
// Validate that the shares-assets ratio is correct
self.enqueue_self.settle_withdraw_private_to_private_internal(assets, shares);

Expand Down Expand Up @@ -653,7 +674,7 @@ pub contract Vault {
// Burn max_shares from the sender's private balance on the shares token
self.call(Token::at(shares_token).burn_private(from, max_shares, nonce));

// Order matters: transfer after burning to neutralize ARC-403 reentrancy.
// Order matters: transfer after burning to order effects safely (necessary but NOT sufficient against a hooked token — see the ARC-403 reentrancy note at the top of this contract).
// Burn the correct amount of shares
// Any excess amount of shares is sent back to the sender via commitment
// Reverts if the amount of shares required is greater than max_shares
Expand Down Expand Up @@ -690,7 +711,7 @@ pub contract Vault {
let assets =
_convert_to_assets(shares, total_assets, total_supply, vault_offset, ROUND_DOWN);

// Order matters: burn before transferring to neutralize ARC-403 reentrancy.
// Order matters: burn before transferring to order effects safely (necessary but NOT sufficient against a hooked token — see the ARC-403 reentrancy note at the top of this contract).
// Burn the sender's shares on the shares token
self.call(Token::at(shares_token).burn_public(from, shares, nonce));

Expand Down Expand Up @@ -744,7 +765,7 @@ pub contract Vault {
// Burn shares from the sender's private balance on the shares token
self.call(Token::at(shares_token).burn_private(from, shares, nonce));

// Order matters: transfer after burning to neutralize ARC-403 reentrancy.
// Order matters: transfer after burning to order effects safely (necessary but NOT sufficient against a hooked token — see the ARC-403 reentrancy note at the top of this contract).
// Transfers any outstanding assets without revealing the recipient
// Reverts if min_assets is greater than allowed
self.enqueue_self.settle_redeem_private_to_private_exact_internal(
Expand Down Expand Up @@ -781,7 +802,7 @@ pub contract Vault {
let asset_commitment =
self.call(Token::at(asset_token).initialize_transfer_commitment(to, self.address));

// Order matters: transfer after burning to neutralize ARC-403 reentrancy.
// Order matters: transfer after burning to order effects safely (necessary but NOT sufficient against a hooked token — see the ARC-403 reentrancy note at the top of this contract).
// Burns shares and transfers any outstanding assets without revealing the recipient
// Reverts if min_assets is greater than allowed
self.enqueue_self.settle_redeem_public_to_private_exact_internal(
Expand Down Expand Up @@ -1081,7 +1102,7 @@ pub contract Vault {

assert(shares <= max_shares, "Too many shares requested");

// Order matters: transfer before minting to neutralize ARC-403 reentrancy.
// 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,
Expand Down Expand Up @@ -1222,7 +1243,7 @@ pub contract Vault {
_convert_to_shares(assets, total_assets, total_supply, vault_offset, ROUND_DOWN);
let outstanding_shares = max_shares - min_shares; // Reverts with underflow if invalid

// Order matters: transfer before minting to neutralize ARC-403 reentrancy.
// 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,
Expand Down
Loading