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
1 change: 1 addition & 0 deletions Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"src/escrow_contract",
"src/escrow_contract/src/test/test_logic_contract",
"src/generic_proxy",
"src/token_contract/src/test/test_authorization_contract",
]

[benchmark]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The `Dripper` contract provides a convenient faucet mechanism for minting tokens

## Token Contract

The `Token` contract implements an ERC-20-like token with Aztec-specific privacy extensions. It supports transfers and interactions explicitly through private balances and public balances, offering full coverage of Aztec's confidentiality features.
The `Token` contract implements an ERC-20-like token with Aztec-specific privacy extensions. It supports transfers and interactions explicitly through private balances and public balances, offering full coverage of Aztec's confidentiality features. Optional ARC-403 authorization hooks allow third-party contracts to gate balance-changing operations.

We published the [AIP-20 Aztec Token Standard](https://forum.aztec.network/t/request-for-comments-aip-20-aztec-token-standard/7737) to the forum. Feel free to review and discuss the specification there.

Expand Down
1 change: 1 addition & 0 deletions src/token_contract/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v
balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.0.0-devnet.2-patch.1", directory = "noir-projects/aztec-nr/balance-set" }
compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.0.0-devnet.2-patch.1", directory = "noir-projects/aztec-nr/compressed-string" }
generic_proxy = { path = "../generic_proxy" }
authorization_contract = { path = "src/test/test_authorization_contract" }
23 changes: 23 additions & 0 deletions src/token_contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ This implementation provides a robust foundation for fungible tokens on Aztec, e

This contract follows the [AIP-20 Aztec Token Standard](https://forum.aztec.network/t/request-for-comments-aip-20-aztec-token-standard/7737). Feel free to review and discuss the specification on the Aztec forum.

## ARC-403: Authorization Hooks

The token supports an optional authorization contract (ARC-403) that is invoked before every balance-changing operation. Set at deployment via `auth_contract`; use the zero address to disable.

| Entry point | Hook called |
|-------------------|-------------------|
| Private functions | `authorize_private` |
| Public functions | `authorize_public` |

Authorization follows the **entry-point context**: private calls invoke the private hook (nothing revealed on-chain), public calls invoke the public hook. Query `get_auth_contract()` for the configured address.

## Transfer Events

The contract emits a public `Transfer { from, to, amount }` event on every balance-changing operation (mints, burns, public transfers, and cross-domain private ↔ public moves), enabling indexers to track token movements.
Expand All @@ -33,6 +44,7 @@ The contract emits a public `Transfer { from, to, amount }` event on every balan
- `public_balances: Map<AztecAddress, u128>`: Public balances per account.
- `total_supply: u128`: Total token supply.
- `minter: AztecAddress`: Authorized minter address (if set).
- `auth_contract: AztecAddress`: ARC-403 authorization hook (zero = disabled).

## Initializer Functions

Expand All @@ -45,6 +57,7 @@ The contract emits a public `Transfer { from, to, amount }` event on every balan
/// @param decimals The number of decimals of the token
/// @param initial_supply The initial supply of the token
/// @param to The address to mint the initial supply to
/// @param auth_contract ARC-403 authorization hook (zero address to disable)
#[public]
#[initializer]
fn constructor_with_initial_supply(
Expand All @@ -63,6 +76,7 @@ fn constructor_with_initial_supply(
/// @param symbol The symbol of the token
/// @param decimals The number of decimals of the token
/// @param minter The address of the minter
/// @param auth_contract ARC-403 authorization hook (zero address to disable)
#[public]
#[initializer]
fn constructor_with_minter(
Expand Down Expand Up @@ -121,6 +135,15 @@ fn symbol() -> FieldCompressedString { /* ... */ }
fn decimals() -> u8 { /* ... */ }
```

### get_auth_contract
```rust
/// @notice Returns the ARC-403 authorization hook contract address
/// @return The auth contract address (zero address means authorization is disabled)
#[public]
#[view]
fn get_auth_contract() -> AztecAddress { /* ... */ }
```

## Utility Functions

### balance_of_private
Expand Down
76 changes: 75 additions & 1 deletion src/token_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub contract Token {
use compressed_string::FieldCompressedString;
// private balance library
use balance_set::BalanceSet;
// authorization hook contract
use authorization_contract::AuthorizationContract;

// gas-optimized max notes for initial transfer call
global INITIAL_TRANSFER_CALL_MAX_NOTES: u32 = 2;
Expand All @@ -50,6 +52,7 @@ pub contract Token {
/// @param total_supply The total supply of the token
/// @param public_balances The public balances of the token
/// @param minter The account permissioned to mint the token
/// @param auth_contract The authorization contract called on every transfer
#[storage]
struct Storage<Context> {
name: PublicImmutable<FieldCompressedString, Context>,
Expand All @@ -59,6 +62,7 @@ pub contract Token {
total_supply: PublicMutable<u128, Context>,
public_balances: Map<AztecAddress, PublicMutable<u128, Context>, Context>,
minter: PublicImmutable<AztecAddress, Context>,
auth_contract: PublicImmutable<AztecAddress, Context>,
}

/// @notice Initializes the token with an initial supply
Expand All @@ -68,6 +72,7 @@ pub contract Token {
/// @param decimals The number of decimals of the token
/// @param initial_supply The initial supply of the token
/// @param to The address to mint the initial supply to
/// @param auth_contract The address of the authorization contract (zero address to disable)
#[external("public")]
#[initializer]
fn constructor_with_initial_supply(
Expand All @@ -76,27 +81,38 @@ pub contract Token {
decimals: u8,
initial_supply: u128,
to: AztecAddress,
auth_contract: AztecAddress,
) {
self.storage.name.initialize(FieldCompressedString::from_string(name));
self.storage.symbol.initialize(FieldCompressedString::from_string(symbol));
self.storage.decimals.initialize(decimals);

self.internal._mint_to_public(to, initial_supply);

self.storage.auth_contract.initialize(auth_contract);
}

/// @notice Initializes the token with a minter
/// @param name The name of the token
/// @param symbol The symbol of the token
/// @param decimals The number of decimals of the token
/// @param minter The address of the minter
/// @param auth_contract The address of the authorization contract (zero address to disable)
#[external("public")]
#[initializer]
fn constructor_with_minter(name: str<31>, symbol: str<31>, decimals: u8, minter: AztecAddress) {
fn constructor_with_minter(
name: str<31>,
symbol: str<31>,
decimals: u8,
minter: AztecAddress,
auth_contract: AztecAddress,
) {
self.storage.name.initialize(FieldCompressedString::from_string(name));
self.storage.symbol.initialize(FieldCompressedString::from_string(symbol));
self.storage.decimals.initialize(decimals);

self.storage.minter.initialize(minter);
self.storage.auth_contract.initialize(auth_contract);
}

/** ==========================================================
Expand All @@ -118,6 +134,8 @@ pub contract Token {
) {
_validate_from_private::<4>(self.context, from);

self.internal._call_auth_private(from, amount);

self.internal._decrease_private_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES);

self.enqueue_self.increase_public_balance_internal(to, amount);
Expand All @@ -140,6 +158,8 @@ pub contract Token {
) -> Field {
_validate_from_private::<4>(self.context, from);

self.internal._call_auth_private(from, amount);

self.internal._decrease_private_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES);

self.enqueue_self.increase_public_balance_internal(to, amount);
Expand All @@ -165,6 +185,8 @@ pub contract Token {
) {
_validate_from_private::<4>(self.context, from);

self.internal._call_auth_private(from, amount);

self.internal._decrease_private_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES);

self.internal._increase_private_balance(to, amount);
Expand All @@ -186,6 +208,8 @@ pub contract Token {
) {
_validate_from_private::<4>(self.context, from);

self.internal._call_auth_private(from, amount);

self.internal._decrease_private_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES);

let completer = self.msg_sender();
Expand All @@ -211,6 +235,8 @@ pub contract Token {
) {
_validate_from_private::<4>(self.context, from);

self.internal._call_auth_private(from, amount);

self.enqueue_self.decrease_public_balance_internal(from, amount);

self.internal._increase_private_balance(to, amount);
Expand Down Expand Up @@ -257,6 +283,8 @@ pub contract Token {
) {
self.internal._validate_from_public(from);

self.internal._call_auth_public(from, amount);

self.internal._decrease_public_balance(from, amount);
self.internal._increase_public_balance(to, amount);

Expand All @@ -280,6 +308,8 @@ pub contract Token {
) {
self.internal._validate_from_public(from);

self.internal._call_auth_public(from, amount);

self.internal._decrease_public_balance(from, amount);

let completer = self.msg_sender();
Expand Down Expand Up @@ -359,6 +389,14 @@ pub contract Token {
self.storage.decimals.read()
}

/// @notice Returns the ARC-403 authorization hook contract address
/// @return The auth contract address (zero address means authorization is disabled)
#[external("public")]
#[view]
fn get_auth_contract() -> AztecAddress {
self.storage.auth_contract.read()
}

/** ==========================================================
* ===================== UNCONSTRAINED =======================
* ======================================================== */
Expand All @@ -383,6 +421,8 @@ pub contract Token {
fn mint_to_private(to: AztecAddress, amount: u128) {
_validate_minter(self.msg_sender(), self.storage.minter.read());

self.internal._call_auth_private(AztecAddress::zero(), amount);

self.internal._mint_to_private(to, amount);
}

Expand All @@ -394,6 +434,8 @@ pub contract Token {
fn mint_to_public(to: AztecAddress, amount: u128) {
_validate_minter(self.msg_sender(), self.storage.minter.read());

self.internal._call_auth_public(AztecAddress::zero(), amount);

self.internal._mint_to_public(to, amount);
}

Expand All @@ -405,6 +447,9 @@ pub contract Token {
fn mint_to_commitment(commitment: Field, amount: u128) {
let sender = self.msg_sender();
_validate_minter(sender, self.storage.minter.read());

self.internal._call_auth_public(AztecAddress::zero(), amount);

let completer = sender;
self.internal._increase_total_supply(amount);
self.internal._increase_commitment_balance(
Expand Down Expand Up @@ -439,6 +484,8 @@ pub contract Token {
fn burn_private(from: AztecAddress, amount: u128, _nonce: Field) {
_validate_from_private::<3>(self.context, from);

self.internal._call_auth_private(from, amount);

self.internal._burn_private(from, amount);
}

Expand All @@ -451,6 +498,8 @@ pub contract Token {
fn burn_public(from: AztecAddress, amount: u128, _nonce: Field) {
self.internal._validate_from_public(from);

self.internal._call_auth_public(from, amount);

self.internal._burn_public(from, amount);
}

Expand All @@ -468,6 +517,31 @@ pub contract Token {
* ================= TOKEN LIBRARIES =========================
* ======================================================== */

/// @notice ARC-403: calls the public authorization hook when one is configured
/// @dev Used for public-source operations (public balance decreases); no-ops when auth_contract is zero
/// @param from The address tokens are moved from (zero address for mints)
/// @param amount The amount of tokens being moved
#[internal("public")]
fn _call_auth_public(from: AztecAddress, amount: u128) {
let auth = self.storage.auth_contract.read();
if !auth.eq(AztecAddress::zero()) {
self.call(AuthorizationContract::at(auth).authorize_public(from, amount));
}
}

/// @notice ARC-403: calls the private authorization hook when one is configured
/// @dev Used for private-source operations (private note spends); no-ops when auth_contract is zero.
/// Runs entirely in private context - the auth call is never revealed on-chain.
/// @param from The address tokens are moved from (zero address for mints)
/// @param amount The amount of tokens being moved
#[internal("private")]
fn _call_auth_private(from: AztecAddress, amount: u128) {
let auth = self.storage.auth_contract.read();
if !auth.eq(AztecAddress::zero()) {
AuthorizationContract::at(auth).authorize_private(from, amount).call(self.context);
}
}

/// @notice Validates that the caller is the minter
/// @param sender The address of the caller
/// @param minter The address of the authorized minter
Expand Down
1 change: 1 addition & 0 deletions src/token_contract/src/test.nr
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod authorization;
mod burn_private;
mod burn_public;
mod mint_to_private;
Expand Down
Loading