Private & Public token contract
In this tutorial we will go through writing an L2 native token contract for the Aztec Network, using the Aztec.nr contract libraries.
This tutorial is intended to help you get familiar with the Aztec.nr library, Aztec contract syntax and some of the underlying structure of the Aztec network.
In this tutorial you will learn how to:
- Write public functions that update public state
- Write private functions that update private state
- Implement access control on public and private functions
- Handle math operations safely
- Handle different private note types
- Pass data between private and public state
We are going to start with a blank project and fill in the token contract source code defined here (GitHub Link), and explain what is being added as we go.
Requirements
You will need to have aztec-nargo
installed in order to compile Aztec.nr contracts.
Project setup
Create a new project with:
aztec-nargo new --contract token_contract
Your file structure should look something like this:
.
|--private_voting
| |--src
| | |--main.nr
| |--Nargo.toml
Inside Nargo.toml
paste the following:
[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.68.0", directory="noir-projects/aztec-nr/aztec" }
authwit={ git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.68.0", directory="noir-projects/aztec-nr/authwit"}
compressed_string = {git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.68.0", directory="noir-projects/aztec-nr/compressed-string"}
uint_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.68.0", directory="noir-projects/aztec-nr/uint-note" }
We will be working within main.nr
for the rest of the tutorial.
How this will work
Before writing the functions, let's go through them to see how this contract will work:
Initializer
There is one initializer
function in this contract, and it will be selected and executed once when the contract is deployed, similar to a constructor in Solidity. This is marked private, so the function logic will not be transparent. To execute public function logic in the constructor, this function will call _initialize
(marked internal, more detail below).
Public functions
These are functions that have transparent logic, will execute in a publicly verifiable context and can update public storage.
set_admin
enables the admin to be updated- [
set_minter](#set_minter)
enables accounts to be added / removed from the approved minter list mint_to_public
enables tokens to be minted to the public balance of an accounttransfer_in_public
enables users to transfer tokens from one account's public balance to another account's public balanceburn_public
enables users to burn tokensfinalize_mint_to_private
finalizes aprepare_private_balance_increase
callfinalize_transfer_to_private
finalizes aprepare_private_balance_increase
call
Private functions
These are functions that have private logic and will be executed on user devices to maintain privacy. The only data that is submitted to the network is a proof of correct execution, new data commitments and nullifiers, so users will not reveal which contract they are interacting with or which function they are executing. The only information that will be revealed publicly is that someone executed a private transaction on Aztec.
transfer
enables an account to send tokens from their private balance to another account's private balancetransfer_in_private
enables an account to send tokens from another account's private balance to another account's private balancetransfer_to_private
transfers a specifiedamount
from an accounts public balance to a designated recipient's private balance. This flow starts in private, but will be completed in public.transfer_to_public
transfers tokens from the private balance of another account, to a (potentially different account's) public balancemint_to_private
enables an authorized minter to mint tokens to a specified addresscancel_authwit
enables an account to cancel an authorization to spend tokensburn_private
enables tokens to be burned privatelysetup_refund
allows users using a fee paying contract to receive unspent transaction feesprepare_private_balance_increase
is used to set up a partial note to be completed in public
Private view
functions
These functions provide an interface to allow other contracts to read state variables in private:
private_get_name
private_get_symbol
private_get_decimals
Internal functions
Internal functions are functions that can only be called by the contract itself. These can be used when the contract needs to call one of it's public functions from one of it's private functions.
_increase_public_balance
increases the public balance of an account whentransfer_to_public
is called_reduce_total_supply
reduces the total supply of tokens when a token is privately burnedcomplete_refund
used in the fee payment flow. There is more detail on the partial note page._finalize_transfer_to_private_unsafe
is the public component for finalizing a transfer from a public balance to private balance. It is consideredunsafe
becausefrom
is not enforced in this function, but it is in enforced the private function that calls this one (so it's safe)._finalize_mint_to_private_unsafe
finalizes a private mint. Like the function above, it is consideredunsafe
becausefrom
is not enforced in this function, but it is in enforced the private function that calls this one (so it's safe).
To clarify, let's review some details of the Aztec transaction lifecycle, particularly how a transaction "moves through" these contexts.
Execution contexts
Transactions are initiated in the private context (executed client-side), then move to the L2 public context (executed remotely by an Aztec sequencer), then to the Ethereum L1 context (executed by an Ethereum node).
Step 1. Private Execution
Users provide inputs and execute locally on their device for privacy reasons. Outputs of the private execution are commitment and nullifier updates, a proof of correct execution and any return data to pass to the public execution context.
Step 2. Public Execution
This happens remotely by the sequencer, which takes inputs from the private execution and runs the public code in the network virtual machine, similar to any other public blockchain.
Step 3. Ethereum execution
Aztec transactions can pass messages to Ethereum contracts through the rollup via the outbox. The data can be consumed by Ethereum contracts at a later time, but this is not part of the transaction flow for an Aztec transaction. The technical details of this are beyond the scope of this tutorial, but we will cover them in an upcoming piece.
Unconstrained functions
Unconstrained functions can be thought of as view functions from Solidity--they only return information from the contract storage or compute and return data without modifying contract storage.
Contract dependencies
Before we can implement the functions, we need set up the contract storage, and before we do that we need to import the appropriate dependencies.
We will be going over the code in main.nr
here (GitHub link). If you are following along and want to compile main.nr
yourself, you need to add the other files in the directory as they contain imports that are used in main.nr
.
Paste these imports:
mod types;
mod test;
use dep::aztec::macros::aztec;
// Minimal token implementation that supports `AuthWit` accounts.
// The auth message follows a similar pattern to the cross-chain message and includes a designated caller.
// The designated caller is ALWAYS used here, and not based on a flag as cross-chain.
// message hash = H([caller, contract, selector, ...args])
// To be read as `caller` calls function at `contract` defined by `selector` with `args`
// Including a nonce in the message hash ensures that the message can only be used once.
#[aztec]
contract Token {
// Libs
use std::meta::derive;
use dep::compressed_string::FieldCompressedString;
use dep::aztec::{
context::{PrivateCallInterface, PrivateContext},
encrypted_logs::{
encrypted_event_emission::encode_and_encrypt_event_unconstrained,
encrypted_note_emission::encode_and_encrypt_note_unconstrained,
},
macros::{
events::event,
functions::{initializer, internal, private, public, view},
storage::storage,
},
oracle::random::random,
prelude::{
AztecAddress, FunctionSelector, Map, PublicContext, PublicImmutable, PublicMutable,
},
protocol_types::{point::Point, traits::Serialize},
};
use dep::uint_note::uint_note::UintNote;
use dep::authwit::auth::{
assert_current_call_valid_authwit, assert_current_call_valid_authwit_public,
compute_authwit_nullifier,
};
use crate::types::balance_set::BalanceSet;
}
We are importing:
CompressedString
to hold the token symbol- Types from
aztec::prelude
- Types for storing note types
Types files
We are also importing types from a types.nr
file, which imports types from the types
folder. You can view them here (GitHub link).
Private state in Aztec is all UTXOs.
Contract Storage
Now that we have dependencies imported into our contract we can define the storage for the contract.
Below the dependencies, paste the following Storage struct:
#[storage]
struct Storage<Context> {
admin: PublicMutable<AztecAddress, Context>,
minters: Map<AztecAddress, PublicMutable<bool, Context>, Context>,
balances: Map<AztecAddress, BalanceSet<Context>, Context>,
total_supply: PublicMutable<U128, Context>,
public_balances: Map<AztecAddress, PublicMutable<U128, Context>, Context>,
symbol: PublicImmutable<FieldCompressedString, Context>,
name: PublicImmutable<FieldCompressedString, Context>,
decimals: PublicImmutable<u8, Context>,
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L69-L89
Reading through the storage variables:
admin
an Aztec address stored in public state.minters
is a mapping of Aztec addresses in public state. This will store whether an account is an approved minter on the contract.balances
is a mapping of private balances. Private balances are stored in aPrivateSet
ofUintNote
s. The balance is the sum of all of an account'sUintNote
s.total_supply
is an unsigned integer (max 128 bit value) stored in public state and represents the total number of tokens minted.public_balances
is a mapping of Aztec addresses in public state and represents the publicly viewable balances of accounts.symbol
,name
, anddecimals
are similar in meaning to ERC20 tokens on Ethereum.
Functions
Copy and paste the body of each function into the appropriate place in your project if you are following along.
Constructor
This function sets the creator of the contract (passed as msg_sender
from the constructor) as the admin and makes them a minter, and sets name, symbol, and decimals.
#[public]
#[initializer]
fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) {
assert(!admin.is_zero(), "invalid admin");
storage.admin.write(admin);
storage.minters.at(admin).write(true);
storage.name.initialize(FieldCompressedString::from_string(name));
storage.symbol.initialize(FieldCompressedString::from_string(symbol));
storage.decimals.initialize(decimals);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L91-L104
Public function implementations
Public functions are declared with the #[public]
macro above the function name.
As described in the execution contexts section above, public function logic and transaction information is transparent to the world. Public functions update public state, but can be used to finalize notes prepared in a private context (partial notes flow).
Storage is referenced as storage.variable
.
set_admin
After storage is initialized, the contract checks that the msg_sender
is the admin
. If not, the transaction will fail. If it is, the new_admin
is saved as the admin
.
#[public]
fn set_admin(new_admin: AztecAddress) {
assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin");
storage.admin.write(new_admin);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L106-L114
set_minter
This function allows the admin
to add or a remove a minter
from the public minters
mapping. It checks that msg_sender
is the admin
and finally adds the minter
to the minters
mapping.
#[public]
fn set_minter(minter: AztecAddress, approve: bool) {
assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin");
storage.minters.at(minter).write(approve);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L184-L194
mint_to_public
This function allows an account approved in the public minters
mapping to create new public tokens owned by the provided to
address.
First, storage is initialized. Then the function checks that the msg_sender
is approved to mint in the minters
mapping. If it is, a new U128
value is created of the amount
provided. The function reads the recipients public balance and then adds the amount to mint, saving the output as new_balance
, then reads to total supply and adds the amount to mint, saving the output as supply
. new_balance
and supply
are then written to storage.
#[public]
fn mint_to_public(to: AztecAddress, amount: Field) {
assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter");
let amount = U128::from_integer(amount);
let new_balance = storage.public_balances.at(to).read().add(amount);
let supply = storage.total_supply.read().add(amount);
storage.public_balances.at(to).write(new_balance);
storage.total_supply.write(supply);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L196-L208
transfer_in_public
This public function enables public transfers between Aztec accounts. The sender's public balance will be debited the specified amount
and the recipient's public balances will be credited with that amount.
Authorizing token spends
If the msg_sender
is NOT the same as the account to debit from, the function checks that the account has authorized the msg_sender
contract to debit tokens on its behalf. This check is done by computing the function selector that needs to be authorized, computing the hash of the message that the account contract has approved. This is a hash of the contract that is approved to spend (context.msg_sender
), the token contract that can be spent from (context.this_address()
), the selector
, the account to spend from (from
), the amount
and a nonce
to prevent multiple spends. This hash is passed to assert_inner_hash_valid_authwit_public
to ensure that the Account Contract has approved tokens to be spent on it's behalf.
If the msg_sender
is the same as the account to debit tokens from, the authorization check is bypassed and the function proceeds to update the account's public_balance
.
#[public]
fn transfer_in_public(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) {
if (!from.eq(context.msg_sender())) {
assert_current_call_valid_authwit_public(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
let amount = U128::from_integer(amount);
let from_balance = storage.public_balances.at(from).read().sub(amount);
storage.public_balances.at(from).write(from_balance);
let to_balance = storage.public_balances.at(to).read().add(amount);
storage.public_balances.at(to).write(to_balance);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L210-L224
burn_public
This public function enables public burning (destroying) of tokens from the sender's public balance.
After storage is initialized, the authorization flow specified above is checked. Then the sender's public balance and the total_supply
are updated and saved to storage.
#[public]
fn burn_public(from: AztecAddress, amount: Field, nonce: Field) {
if (!from.eq(context.msg_sender())) {
assert_current_call_valid_authwit_public(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
let amount = U128::from_integer(amount);
let from_balance = storage.public_balances.at(from).read().sub(amount);
storage.public_balances.at(from).write(from_balance);
let new_supply = storage.total_supply.read().sub(amount);
storage.total_supply.write(new_supply);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L226-L242
finalize_mint_to_private
This public function finalizes a transfer that has been set up by a call to prepare_private_balance_increase
by reducing the public balance of the associated account and emitting the note for the intended recipient.
/// Finalizes a mint of token `amount` to a private balance of `to`. The mint must be prepared by calling
/// `prepare_private_balance_increase` first and the resulting
/// `hiding_point_slot` must be passed as an argument to this function.
///
/// Note: This function is only an optimization as it could be replaced by a combination of `mint_to_public`
/// and `finalize_transfer_to_private`. It is however used very commonly so it makes sense to optimize it
/// (e.g. used during token bridging, in AMM liquidity token etc.).
#[public]
fn finalize_mint_to_private(amount: Field, hiding_point_slot: Field) {
assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter");
_finalize_mint_to_private(amount, hiding_point_slot, &mut context, storage);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L568-L582
finalize_transfer_to_private
Similar to finalize_mint_to_private
, this public function finalizes a transfer that has been set up by a call to prepare_private_balance_increase
by reducing the public balance of the associated account and emitting the note for the intended recipient.
/// Finalizes a transfer of token `amount` from public balance of `from` to a private balance of `to`.
/// The transfer must be prepared by calling `prepare_private_balance_increase` first and the resulting
/// `hiding_point_slot` must be passed as an argument to this function.
#[public]
fn finalize_transfer_to_private(amount: Field, hiding_point_slot: Field) {
let from = context.msg_sender();
_finalize_transfer_to_private(from, amount, hiding_point_slot, &mut context, storage);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L496-L505
Private function implementations
Private functions are declared with the #[private]
macro above the function name like so:
#[private]
fn transfer_to_public(
As described in the execution contexts section above, private function logic and transaction information is hidden from the world and is executed on user devices. Private functions update private state, but can pass data to the public execution context (e.g. see the transfer_to_public
function).
Storage is referenced as storage.variable
.
transfer_to_public
This private function enables transferring of private balance (UintNote
stored in balances
) to any Aztec account's public_balance
.
After initializing storage, the function checks that the msg_sender
is authorized to spend tokens. See the Authorizing token spends section above for more detail--the only difference being that assert_inner_hash_valid_authwit
in the authwit check is modified to work specifically in the private context. After the authorization check, the sender's private balance is decreased using the decrement
helper function for the value_note
library. Then it stages a public function call on this contract (_increase_public_balance
) to be executed in the public execution phase of transaction execution. _increase_public_balance
is marked as an internal
function, so can only be called by this token contract.
The function returns 1
to indicate successful execution.
#[private]
fn transfer_to_public(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) {
if (!from.eq(context.msg_sender())) {
assert_current_call_valid_authwit(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
// TODO: constrain encryption below - we are using unconstrained here only because of the following Noir issue
// https://github.com/noir-lang/noir/issues/5771
storage.balances.at(from).sub(from, U128::from_integer(amount)).emit(
encode_and_encrypt_note_unconstrained(&mut context, from, from),
);
Token::at(context.this_address())._increase_public_balance(to, amount).enqueue(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L244-L260
transfer
This private function enables private token transfers between Aztec accounts.
After initializing storage, the function checks that the msg_sender
is authorized to spend tokens. See the Authorizing token spends section above for more detail--the only difference being that assert_valid_message_for
is modified to work specifically in the private context. After authorization, the function gets the current balances for the sender and recipient and decrements and increments them, respectively, using the value_note
helper functions.
#[private]
fn transfer(to: AztecAddress, amount: Field) {
let from = context.msg_sender();
let amount = U128::from_integer(amount);
// We reduce `from`'s balance by amount by recursively removing notes over potentially multiple calls. This
// method keeps the gate count for each individual call low - reading too many notes at once could result in
// circuits in which proving is not feasible.
// Since the sum of the amounts in the notes we nullified was potentially larger than amount, we create a new
// note for `from` with the change amount, e.g. if `amount` is 10 and two notes are nullified with amounts 8 and
// 5, then the change will be 3 (since 8 + 5 - 10 = 3).
let change = subtract_balance(
&mut context,
storage,
from,
amount,
INITIAL_TRANSFER_CALL_MAX_NOTES,
);
storage.balances.at(from).add(from, change).emit(encode_and_encrypt_note_unconstrained(
&mut context,
from,
from,
));
storage.balances.at(to).add(to, amount).emit(encode_and_encrypt_note_unconstrained(
&mut context,
to,
from,
));
// We don't constrain encryption of the note log in `transfer` (unlike in `transfer_in_private`) because the transfer
// function is only designed to be used in situations where the event is not strictly necessary (e.g. payment to
// another person where the payment is considered to be successful when the other party successfully decrypts a
// note).
Transfer { from, to, amount: amount.to_field() }.emit(
encode_and_encrypt_event_unconstrained(&mut context, to, from),
);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L262-L299
transfer_in_private
This private function enables an account to transfer tokens on behalf of another account. The account that tokens are being debited from must have authorized the msg_sender
to spend tokens on its behalf.
#[private]
fn transfer_in_private(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) {
if (!from.eq(context.msg_sender())) {
assert_current_call_valid_authwit(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
let amount = U128::from_integer(amount);
// TODO: constrain encryption below - we are using unconstrained here only becuase of the following Noir issue
// https://github.com/noir-lang/noir/issues/5771
storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note_unconstrained(
&mut context,
from,
from,
));
// TODO: constrain encryption below - we are using unconstrained here only becuase of the following Noir issue
// https://github.com/noir-lang/noir/issues/5771
storage.balances.at(to).add(to, amount).emit(encode_and_encrypt_note_unconstrained(
&mut context,
to,
from,
));
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L365-L396
transfer_to_private
This function execution flow starts in the private context and is completed with a call to a public internal function. It enables an account to send tokens from its public_balance
to a private balance of an arbitrary recipient.
First a partial note is prepared then a call to the public, internal _finalize_transfer_to_private_unsafe
is enqueued. The enqueued public call subtracts the amount
from public balance of msg_sender
and finalizes the partial note with the amount
.
// Transfers token `amount` from public balance of message sender to a private balance of `to`.
#[private]
fn transfer_to_private(to: AztecAddress, amount: Field) {
// `from` is the owner of the public balance from which we'll subtract the `amount`.
let from = context.msg_sender();
let token = Token::at(context.this_address());
// We prepare the private balance increase (the partial note).
let hiding_point_slot = _prepare_private_balance_increase(from, to, &mut context, storage);
// At last we finalize the transfer. Usage of the `unsafe` method here is safe because we set the `from`
// function argument to a message sender, guaranteeing that he can transfer only his own tokens.
token._finalize_transfer_to_private_unsafe(from, amount, hiding_point_slot).enqueue(
&mut context,
);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L415-L432
mint_to_private
This private function prepares a partial UintNote
at the recipients storage slot in the contract and enqueues a public call to _finalize_mint_to_private_unsafe
, which asserts that the msg_sender
is an authorized minter and finalized the mint by incrementing the total supply and emitting the complete, encrypted UintNote
to the intended recipient. Note that the amount
and the minter (from
) are public, but the recipient is private.
/// Mints token `amount` to a private balance of `to`. Message sender has to have minter permissions (checked
/// in the enqueued call).
#[private]
fn mint_to_private(
from: AztecAddress, // sender of the tag: TODO(#9887): this is not great?
to: AztecAddress,
amount: Field,
) {
let token = Token::at(context.this_address());
// We prepare the partial note to which we'll "send" the minted amount.
let hiding_point_slot = _prepare_private_balance_increase(from, to, &mut context, storage);
// At last we finalize the mint. Usage of the `unsafe` method here is safe because we set the `from`
// function argument to a message sender, guaranteeing that only a message sender with minter permissions
// can successfully execute the function.
token
._finalize_mint_to_private_unsafe(context.msg_sender(), amount, hiding_point_slot)
.enqueue(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L545-L566
cancel_authwit
This private function allows a user to cancel an authwit that was previously granted. This is achieved by emitting the corresponding nullifier before it is used.
#[private]
fn cancel_authwit(inner_hash: Field) {
let on_behalf_of = context.msg_sender();
let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash);
context.push_nullifier(nullifier);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L356-L363
burn_private
This private function enables accounts to privately burn (destroy) tokens.
After initializing storage, the function checks that the msg_sender
is authorized to spend tokens. Then it gets the sender's current balance and decrements it. Finally it stages a public function call to _reduce_total_supply
.
#[private]
fn burn_private(from: AztecAddress, amount: Field, nonce: Field) {
if (!from.eq(context.msg_sender())) {
assert_current_call_valid_authwit(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
// TODO: constrain encryption below - we are using unconstrained here only becuase of the following Noir issue
// https://github.com/noir-lang/noir/issues/5771
storage.balances.at(from).sub(from, U128::from_integer(amount)).emit(
encode_and_encrypt_note_unconstrained(&mut context, from, from),
);
Token::at(context.this_address())._reduce_total_supply(amount).enqueue(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L398-L413
setup_refund
This private function may be called by a Fee Paying Contract (FPC) in order to allow users to pay transaction fees privately on the network. This function ensures that the user has enough funds in their account to pay the transaction fees for the transaction, sets up partial notes for paying the fees to the fee_payer
and sending any unspent fees back to the user, and enqueues a call to the internal, public complete_refund
function to be run as part of the public execution step.
/// Called by fee payer contract (FPC) to set up a refund for a user during the private fee payment flow.
#[private]
fn setup_refund(
user: AztecAddress, // A user for which we are setting up the fee refund.
max_fee: Field, // The maximum fee a user is willing to pay for the tx.
nonce: Field, // A nonce to make authwitness unique.
) {
// 1. This function is called by FPC when setting up a refund so we need to support the authwit flow here
// and check that the user really permitted fee_recipient to set up a refund on their behalf.
assert_current_call_valid_authwit(&mut context, user);
// 2. Deduct the max fee from user's balance. The difference between max fee and the actual tx fee will
// be refunded to the user in the `complete_refund(...)` function.
let change = subtract_balance(
&mut context,
storage,
user,
U128::from_integer(max_fee),
INITIAL_TRANSFER_CALL_MAX_NOTES,
);
// Emit the change note.
storage.balances.at(user).add(user, change).emit(encode_and_encrypt_note_unconstrained(
&mut context,
user,
user,
));
// 3. Prepare the partial note for the refund.
let user_point_slot = _prepare_private_balance_increase(user, user, &mut context, storage);
// 4. Set the public teardown function to `complete_refund(...)`. Public teardown is the only time when a public
// function has access to the final transaction fee, which is needed to compute the actual refund amount.
let fee_recipient = context.msg_sender(); // FPC is the fee recipient.
context.set_public_teardown_function(
context.this_address(),
comptime { FunctionSelector::from_signature("complete_refund((Field),Field,Field)") },
[fee_recipient.to_field(), user_point_slot, max_fee],
);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L619-L659
prepare_private_balance_increase
TODO: update from prepare_transfer_to_private
This private function prepares to transfer from a public balance to a private balance by setting up a partial note for the recipient. The function returns the hiding_point_slot
. After this, the public finalize_transfer_to_private
must be called, passing the amount and the hiding point slot.
/// Prepares an increase of private balance of `to` (partial note). The increase needs to be finalized by calling
/// some of the finalization functions (`finalize_transfer_to_private`, `finalize_mint_to_private`).
/// Returns a hiding point slot.
#[private]
fn prepare_private_balance_increase(to: AztecAddress, from: AztecAddress) -> Field {
// TODO(#9887): ideally we'd not have `from` here, but we do need a `from` address to produce a tagging secret
// with `to`.
_prepare_private_balance_increase(from, to, &mut context, storage)
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L434-L444
Internal function implementations
Internal functions are functions that can only be called by this contract. The following 3 functions are public functions that are called from the private execution context. Marking these as internal
ensures that only the desired private functions in this contract are able to call them. Private functions defer execution to public functions because private functions cannot update public state directly.
_increase_public_balance
This function is called from transfer_to_public
. The account's private balance is decremented in transfer_to_public
and the public balance is increased in this function.
/// TODO(#9180): Consider adding macro support for functions callable both as an entrypoint and as an internal
/// function.
#[public]
#[internal]
fn _increase_public_balance(to: AztecAddress, amount: Field) {
_increase_public_balance_inner(to, amount, storage);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L711-L719
_reduce_total_supply
This function is called from burn
. The account's private balance is decremented in burn
and the public total_supply
is reduced in this function.
#[public]
#[internal]
fn _reduce_total_supply(amount: Field) {
// Only to be called from burn.
let new_supply = storage.total_supply.read().sub(U128::from_integer(amount));
storage.total_supply.write(new_supply);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L731-L739
complete_refund
This public function is intended to be called during the public teardown at the end of public transaction execution. The call to this function is staged in setup_refund
. This function ensures that the user has sufficient funds to cover the transaction costs and emits encrypted notes to the fee payer and the remaining, unused transaction fee back to the user.
/// Executed as a public teardown function and is responsible for completing the refund in a private fee payment
/// flow.
#[public]
#[internal]
fn complete_refund(fee_recipient: AztecAddress, user_slot: Field, max_fee: Field) {
// TODO(#7728): Remove the next line
let max_fee = U128::from_integer(max_fee);
let tx_fee = U128::from_integer(context.transaction_fee());
// 1. We check that user funded the fee payer contract with at least the transaction fee.
// TODO(#7796): we should try to prevent reverts here
assert(max_fee >= tx_fee, "max fee not enough to cover tx fee");
// 2. We compute the refund amount as the difference between funded amount and the tx fee.
// TODO(#10805): Introduce a real exchange rate
let refund_amount = max_fee - tx_fee;
// 3. We send the tx fee to the fee recipient in public.
_increase_public_balance_inner(fee_recipient, tx_fee.to_field(), storage);
// 4. We construct the user note finalization payload with the refund amount.
let user_finalization_payload =
UintNote::finalization_payload().new(&mut context, user_slot, refund_amount);
// 5. At last we emit the user finalization note hash and the corresponding note log.
user_finalization_payload.emit();
// --> Once the tx is settled user and fee recipient can add the notes to their pixies.
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L679-L708
_finalize_transfer_to_private_unsafe
This public internal function decrements the public balance of the from
account and finalizes the partial note for the recipient, which is hidden in the hiding_point_slot
.
This function is called by the private function transfer_to_private
to finalize the transfer. The transfer_to_private
enforces the from
argument, which is why using it unsafe
is okay.
/// This is a wrapper around `_finalize_transfer_to_private` placed here so that a call
/// to `_finalize_transfer_to_private` can be enqueued. Called unsafe as it does not check `from` (this has to be
/// done in the calling function).
#[public]
#[internal]
fn _finalize_transfer_to_private_unsafe(
from: AztecAddress,
amount: Field,
hiding_point_slot: Field,
) {
_finalize_transfer_to_private(from, amount, hiding_point_slot, &mut context, storage);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L507-L520
_finalize_mint_to_private_unsafe
Similar to _finalize_transfer_to_private_unsafe
, this public internal function increments the private balance of the recipient by finalizing the partial note and emitting the encrypted note. It also increments the public total supply and ensures that the sender of the transaction is authorized to mint tokens on the contract.
#[public]
#[internal]
fn _finalize_mint_to_private_unsafe(
from: AztecAddress,
amount: Field,
hiding_point_slot: Field,
) {
// We check the minter permissions as it was not done in `mint_to_private` function.
assert(storage.minters.at(from).read(), "caller is not minter");
_finalize_mint_to_private(amount, hiding_point_slot, &mut context, storage);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L584-L596
View function implementations
View functions in Aztec are similar to view
functions in Solidity in that they only return information from the contract storage or compute and return data without modifying contract storage. These functions are different from unconstrained functions in that the return values are constrained by their definition in the contract.
Public view calls that are part of a transaction will be executed by the sequencer when the transaction is being executed, so they are not private and will reveal information about the transaction. Private view calls can be safely used in private transactions for getting the same information.
admin
A getter function for reading the public admin
value.
#[public]
#[view]
fn get_admin() -> Field {
storage.admin.read().to_field()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L152-L158
is_minter
A getter function for checking the value of associated with a minter
in the public minters
mapping.
#[public]
#[view]
fn is_minter(minter: AztecAddress) -> bool {
storage.minters.at(minter).read()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L160-L166
total_supply
A getter function for checking the token total_supply
.
#[public]
#[view]
fn total_supply() -> Field {
storage.total_supply.read().to_integer()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L168-L174
balance_of_public
A getter function for checking the public balance of the provided Aztec account.
#[public]
#[view]
fn balance_of_public(owner: AztecAddress) -> Field {
storage.public_balances.at(owner).read().to_integer()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L176-L182
Unconstrained function implementations
Unconstrained functions are similar to view
functions in Solidity in that they only return information from the contract storage or compute and return data without modifying contract storage. They are different from view functions in that the values are returned from the user's PXE and are not constrained by the contract's definition--if there is bad data in the user's PXE, they will get bad data back.
balance_of_private
A getter function for checking the private balance of the provided Aztec account. Note that the Private Execution Environment (PXE) (GitHub link) must have ivsk
(incoming viewing secret key) in order to decrypt the notes.
pub(crate) unconstrained fn balance_of_private(owner: AztecAddress) -> pub Field {
storage.balances.at(owner).balance_of().to_field()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L742-L746
Compiling
Now that the contract is complete, you can compile it with aztec-nargo
. See the Sandbox reference page for instructions on setting it up.
Run the following command in the directory where your Nargo.toml
file is located:
aztec-nargo compile
Once your contract is compiled, optionally generate a typescript interface with the following command:
aztec codegen target -o src/artifacts
Next Steps
Token Bridge Contract
The token bridge tutorial is a great follow up to this one.
It builds on the Token contract described here and goes into more detail about Aztec contract composability and Ethereum (L1) and Aztec (L2) cross-chain messaging.