Private 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 on Github here, 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. See the sandbox reference for installation instructions.
You should also install the Noir Language Support extension for VS Code.
Check the Dev Tools section of the awesome-noir repo for language support for additional editors (Vim, emacs, tree-sitter, etc).
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.44.0", directory="noir-projects/aztec-nr/aztec" }
authwit={ git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.44.0", directory="noir-projects/aztec-nr/authwit"}
compressed_string = {git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.44.0", directory="noir-projects/aztec-nr/compressed-string"}
We will be working within main.nr
for the rest of the tutorial.
Contract Interface
Remove everything from main.nr
and paste this:
contract Token {
#[aztec(public)]
#[aztec(initializer)]
fn constructor() {}
#[aztec(public)]
fn set_admin(new_admin: AztecAddress) {}
#[aztec(public)]
fn set_minter(minter: AztecAddress, approve: bool) {}
#[aztec(public)]
fn mint_public(to: AztecAddress, amount: Field) -> Field {}
#[aztec(public)]
fn mint_private(amount: Field, secret_hash: Field) -> Field {}
#[aztec(public)]
fn shield(from: AztecAddress, amount: Field, secret_hash: Field, nonce: Field) -> Field {}
#[aztec(public)]
fn transfer_public(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) -> Field {}
#[aztec(public)]
fn burn_public(from: AztecAddress, amount: Field, nonce: Field) -> Field {}
// Private functions
#[aztec(private)]
fn redeem_shield(to: AztecAddress, amount: Field, secret: Field) -> Field {}
#[aztec(private)]
fn unshield(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) -> Field {}
#[aztec(private)]
fn transfer(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) -> Field {}
#[aztec(private)]
fn transfer_from(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) {}
#[aztec(private)]
fn cancel_authwit(inner_hash: Field) {}
#[aztec(private)]
fn burn(from: AztecAddress, amount: Field, nonce: Field) -> Field {}
// Internal functions
#[aztec(internal)]
#[aztec(public)]
fn _increase_public_balance(to: AztecAddress, amount: Field) {}
#[aztec(internal)]
#[aztec(public)]
fn _reduce_total_supply(amount: Field) {}
// View functions
#[aztec(public)]
#[aztec(view)]
fn public_get_name() -> pub FieldCompressedString {}
#[aztec(private)]
#[aztec(view)]
fn private_get_name() -> pub FieldCompressedString {}
#[aztec(public)]
#[aztec(view)]
fn public_get_symbol() -> pub FieldCompressedString {}
#[aztec(public)]
#[aztec(view)]
fn public_get_decimals() -> pub u8 {}
#[aztec(private)]
#[aztec(view)]
fn private_get_decimals() -> pub u8 {}
#[aztec(public)]
#[aztec(view)]
fn admin() -> Field {}
#[aztec(public)]
#[aztec(view)]
fn is_minter(minter: AztecAddress) -> bool {}
#[aztec(public)]
#[aztec(view)]
fn total_supply() -> Field {}
#[aztec(public)]
#[aztec(view)]
fn balance_of_public(owner: AztecAddress) -> Field {}
// Unconstrained functions (read only)
unconstrained fn balance_of_private(owner: AztecAddress) -> Field {}
}
This specifies the interface of the Token
contract. Don't worry if you get some warnings - we haven't imported our types yet.
Before we through the interface and implement each function, let's review the functions to get a sense of what the contract does.
Initializer interface
There is one initilizer
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 updatedset_minter
enables accounts to be added / removed from the approved minter listmint_public
enables tokens to be minted to the public balance of an accountmint_private
enables tokens to be minted to the private balance of an account (with some caveats we will dig into)shield
enables tokens to be moved from a public balance to a private balance, not necessarily the same account (step 1 of a 2 step process)transfer_public
enables users to transfer tokens from one account's public balance to another account's public balanceburn_public
enables users to burn tokens
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.
redeem_shield
enables accounts to claim tokens that have been made private viamint_private
orshield
by providing the secretunshield
enables an account to send tokens from their private balance to any other account's public balancetransfer
enables an account to send tokens from their private balance to another account's private balancetransferFrom
enables an account to send tokens from another account's private balance to another account's private balancecancel_authwit
enables an account to cancel an authorization to spend tokensburn
enables tokens to be burned privately
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 whenunshield
is called_reduce_total_supply
reduces the total supply of tokens when a token is privately burned
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, then move to the L2 public context, then to the Ethereum L1 context.
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 data to Ethereum contracts through the rollup via the outbox. The data can 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. 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
.
Just below the contract definition, add the following imports:
mod types;
mod test;
// 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.
contract Token {
// Libs
use dep::compressed_string::FieldCompressedString;
use dep::aztec::{
hash::compute_secret_hash,
prelude::{NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress},
encrypted_logs::encrypted_note_emission::{encode_and_encrypt_note, encode_and_encrypt_note_with_keys}
};
use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier};
use crate::types::{transparent_note::TransparentNote, token_note::{TokenNote, TOKEN_NOTE_LEN}, balances_map::BalancesMap};
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L2-L29
We are importing:
CompressedString
to hold the token symbol- Types from
aztec::prelude
compute_secret_hash
that will help with the shielding and unshielding, allowing someone to claim a token from private to public- Types for storing note types
For more detail on execution contexts, see Contract Communication.
Types files
We are also importing types from a types.nr
file, which imports types from the types
folder. You can view them here.
The main thing to note from this types folder is the TransparentNote
definition. This defines how the contract moves value from the public domain into the private domain. It is similar to the value_note
that we imported, but with some modifications namely, instead of a defined nullifier key, it allows anyone that can produce the pre-image to the stored secret_hash
to spend the note.
Note on private state
Private state in Aztec is all UTXOs to learn more about public and private state in Aztec.
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:
#[aztec(storage)]
struct Storage {
admin: PublicMutable<AztecAddress>,
minters: Map<AztecAddress, PublicMutable<bool>>,
balances: BalancesMap<TokenNote>,
total_supply: PublicMutable<U128>,
pending_shields: PrivateSet<TransparentNote>,
public_balances: Map<AztecAddress, PublicMutable<U128>>,
symbol: SharedImmutable<FieldCompressedString>,
name: SharedImmutable<FieldCompressedString>,
decimals: SharedImmutable<u8>,
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L31-L54
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
ofTokenNote
s. The balance is the sum of all of an account'sTokenNote
s.total_supply
is an unsigned integer (max 128 bit value) stored in public state and represents the total number of tokens minted.pending_shields
is aPrivateSet
ofTransparentNote
s stored in private state. What is stored publicly is a set of commitments toTransparentNote
s.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.
You can read more about it here.
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.
#[aztec(public)]
#[aztec(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#L56-L69
Public function implementations
Public functions are declared with the #[aztec(public)]
macro above the function name like so:
#[aztec(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#L71-L79
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 prepare data to be used in a private context, as we will go over below (e.g. see the shield function).
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
.
#[aztec(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#L71-L79
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.
#[aztec(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#L153-L163
mint_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.
The function returns 1 to indicate successful execution.
#[aztec(public)]
fn mint_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#L165-L178
mint_private
This public function allows an account approved in the public minters
mapping to create new private tokens that can be claimed by anyone that has the pre-image to the secret_hash
.
First, public storage is initialized. Then it checks that the msg_sender
is an approved minter. Then a new TransparentNote
is created with the specified amount
and secret_hash
. You can read the details of the TransparentNote
in the types.nr
file here. The amount
is added to the existing public total_supply
and the storage value is updated. Then the new TransparentNote
is added to the pending_shields
using the insert_from_public
function, which is accessible on the PrivateSet
type. Then it's ready to be claimed by anyone with the secret_hash
pre-image using the redeem_shield
function. It returns 1
to indicate successful execution.
#[aztec(public)]
fn mint_private(amount: Field, secret_hash: Field) {
assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter");
let pending_shields = storage.pending_shields;
let mut note = TransparentNote::new(amount, secret_hash);
let supply = storage.total_supply.read().add(U128::from_integer(amount));
storage.total_supply.write(supply);
pending_shields.insert_from_public(&mut note);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L180-L193
shield
This public function enables an account to stage tokens from it's public_balance
to be claimed as a private TransparentNote
by any account that has the pre-image to the secret_hash
.
First, storage is initialized. Then it checks whether the calling contract (context.msg_sender
) matches the account that the funds will be debited from.
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 (in this case, the shield
function), 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.address
), the amount
, the secret_hash
and a nonce
to prevent multiple spends. This hash is passed to assert_valid_public_message_for
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
and adds a new TransparentNote
to the pending_shields
.
It returns 1
to indicate successful execution.
#[aztec(public)]
fn shield(from: AztecAddress, amount: Field, secret_hash: Field, nonce: Field) {
if (!from.eq(context.msg_sender())) {
// The redeem is only spendable once, so we need to ensure that you cannot insert multiple shields from the same message.
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);
let pending_shields = storage.pending_shields;
let mut note = TransparentNote::new(amount.to_field(), secret_hash);
storage.public_balances.at(from).write(from_balance);
pending_shields.insert_from_public(&mut note);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L212-L231
transfer_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.
After storage is initialized, the authorization flow specified above is checked. Then the sender and recipient's balances are updated and saved to storage.
#[aztec(public)]
fn transfer_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#L233-L249
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.
#[aztec(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#L251-L269
Private function implementations
Private functions are declared with the #[aztec(private)]
macro above the function name like so:
#[aztec(private)]
fn redeem_shield(
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 unshield
function).
Storage is referenced as storage.variable
.
redeem_shield
This private function enables an account to move tokens from a TransparentNote
in the pending_shields
mapping to a TokenNote
in private balances
. The TokenNote
will be associated with a nullifier key, so any account that knows this key can spend this note.
Going through the function logic, first the secret_hash
is generated from the given secret. This ensures that only the entity possessing the secret can use it to redeem the note. Following this, a TransparentNote
is retrieved from the set, using the provided amount and secret. The note is subsequently removed from the set, allowing it to be redeemed only once. The recipient's private balance is then increased using the increment
helper function from the value_note
library.
The function returns 1
to indicate successful execution.
#[aztec(private)]
fn redeem_shield(to: AztecAddress, amount: Field, secret: Field) {
let pending_shields = storage.pending_shields;
let secret_hash = compute_secret_hash(secret);
// Get 1 note (set_limit(1)) which has amount stored in field with index 0 (select(0, amount)) and secret_hash
// stored in field with index 1 (select(1, secret_hash)).
let mut options = NoteGetterOptions::new();
options = options.select(TransparentNote::properties().amount, amount, Option::none()).select(
TransparentNote::properties().secret_hash,
secret_hash,
Option::none()
).set_limit(1);
let notes = pending_shields.get_notes(options);
let note = notes.get_unchecked(0);
// Remove the note from the pending shields set
pending_shields.remove(note);
// Add the token note to user's balances set
// Note: Using context.msg_sender() as a sender below makes this incompatible with escrows because we send
// outgoing logs to that address and to send outgoing logs you need to get a hold of ovsk_m.
let from = context.msg_sender();
storage.balances.add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, from, to));
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L271-L295
unshield
This private function enables un-shielding of private TokenNote
s 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_valid_message_for
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.
#[aztec(private)]
fn unshield(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");
}
storage.balances.sub(from, U128::from_integer(amount)).emit(encode_and_encrypt_note(&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#L297-L310
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.
#[aztec(private)]
fn transfer(to: AztecAddress, amount: Field) {
let from = context.msg_sender();
// By fetching the keys here, we can avoid doing an extra read from the storage, since from_ovpk would
// be needed twice.
let header = context.get_header();
let from_ovpk = header.get_ovpk_m(&mut context, from);
let from_ivpk = header.get_ivpk_m(&mut context, from);
let to_ivpk = header.get_ivpk_m(&mut context, to);
let amount = U128::from_integer(amount);
storage.balances.sub(from, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, from_ivpk));
storage.balances.add(to, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, to_ivpk));
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L312-L328
transfer_from
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.
#[aztec(private)]
fn transfer_from(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");
}
// By fetching the keys here, we can avoid doing an extra read from the storage, since from_ovpk would
// be needed twice.
let header = context.get_header();
let from_ovpk = header.get_ovpk_m(&mut context, from);
let from_ivpk = header.get_ivpk_m(&mut context, from);
let to_ivpk = header.get_ivpk_m(&mut context, to);
let amount = U128::from_integer(amount);
storage.balances.sub(from, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, from_ivpk));
storage.balances.add(to, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, to_ivpk));
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L343-L369
burn
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
.
#[aztec(private)]
fn burn(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");
}
storage.balances.sub(from, U128::from_integer(amount)).emit(encode_and_encrypt_note(&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#L371-L384
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 unshield
. The account's private balance is decremented in shield
and the public balance is increased in this function.
#[aztec(public)]
#[aztec(internal)]
fn _increase_public_balance(to: AztecAddress, amount: Field) {
let new_balance = storage.public_balances.at(to).read().add(U128::from_integer(amount));
storage.public_balances.at(to).write(new_balance);
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L388-L395
_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.
#[aztec(public)]
#[aztec(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#L397-L405
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.
admin
A getter function for reading the public admin
value.
#[aztec(public)]
#[aztec(view)]
fn admin() -> Field {
storage.admin.read().to_field()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L121-L127
is_minter
A getter function for checking the value of associated with a minter
in the public minters
mapping.
#[aztec(public)]
#[aztec(view)]
fn is_minter(minter: AztecAddress) -> bool {
storage.minters.at(minter).read()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L129-L135
total_supply
A getter function for checking the token total_supply
.
#[aztec(public)]
#[aztec(view)]
fn total_supply() -> Field {
storage.total_supply.read().to_integer()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L137-L143
balance_of_public
A getter function for checking the public balance of the provided Aztec account.
#[aztec(public)]
#[aztec(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#L145-L151
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) must have ivsk_app
(app-specific secret key) in order to decrypt the notes.
unconstrained fn balance_of_private(owner: AztecAddress) -> pub Field {
storage.balances.balance_of(owner).to_field()
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L409-L413
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-builder target -o src/artifacts
Next Steps
Testing
Review the end to end tests for reference:
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.