Writing a token contract in Aztec.nr
In this tutorial we will go through writing an L2 native token contract for the Aztec Network, using the Aztec.nr contract libraries. It is recommended that you go through the the introduction to contracts and setup instructions section before this tutorial to gain some familiarity with writing Aztec smart contracts.
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-cli
installed in order to compile Aztec.nr contracts. See the quickstart guide for installation instructions.
When you are running the Sandbox, aztec-cli
, and compiling contracts with Noir, make sure you are using matching versions--we will be shipping breaking changes so mis-matched versions may not work.
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 directory called token_contract_tutorial
mkdir token_contract_tutorial
inside that directory, create a contracts
folder for the Aztec contracts.
cd token_contract_tutorial && mkdir contracts && cd contracts
Create the following file structure
.
└── contracts
├── Nargo.toml
└── src
└── main.nr
Add the following content to Nargo.toml file:
[package]
name = "token_contract"
authors = [""]
compiler_version = ">=0.18.0"
type = "contract"
[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.16.1", directory="yarn-project/aztec-nr/aztec" }
safe_math = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.16.1", directory="yarn-project/aztec-nr/safe-math"}
authwit={ git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.16.1", directory="yarn-project/aztec-nr/authwit"}
Contract Interface
contract Token {
#[aztec(private)]
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 burn(from: AztecAddress, amount: Field, nonce: Field) -> Field {}
// Internal functions below
// Will be internal in the future
#[aztec(public)]
fn _initialize(new_admin: AztecAddress) {}
#[aztec(public)]
internal fn _increase_public_balance(to: AztecAddress, amount: Field) {}
#[aztec(public)]
internal fn _reduce_total_supply(amount: Field) {}
// Unconstrained functions (read only)
unconstrained fn admin() -> Field {}
unconstrained fn is_minter(minter: AztecAddress) -> bool {}
unconstrained fn total_supply() -> Field {}
unconstrained fn balance_of_private(owner: AztecAddress) -> Field {}
unconstrained fn balance_of_public(owner: AztecAddress) -> Field {}
unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, serialized_note: [Field; VALUE_NOTE_LEN]) -> [Field; 4] {}
}
This specifies the interface of the Token
contract. Go ahead and copy and paste this interface into your main.nr
file.
Before we through the interface and implement each function, let's review the functions to get a sense of what the contract does.
Constructor interface
There is a constructor
function that will be executed once, when the contract is deployed, similar to the constructor function 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 balanceburn
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.
_initialize
is a way to call a public function from theconstructor
(which is a private function)_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 a 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.
The compute_note_hash_and_nullifier
function allows contract devs to specify how to compute notes and nullifiers. This must be included in every contract because it depends on the storage slots, which are defined when we set up 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;
// 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::std::option::Option;
use dep::safe_math::SafeU120;
use dep::aztec::{
note::{
note_getter_options::NoteGetterOptions,
note_header::NoteHeader,
utils as note_utils,
},
context::{PrivateContext, PublicContext, Context},
hash::{compute_secret_hash},
state_vars::{map::Map, public_state::PublicState, set::Set},
types::type_serialization::{
field_serialization::{FieldSerializationMethods, FIELD_SERIALIZED_LEN},
bool_serialization::{BoolSerializationMethods, BOOL_SERIALIZED_LEN},
aztec_address_serialization::{AztecAddressSerializationMethods, AZTEC_ADDRESS_SERIALIZED_LEN},
},
types::address::{AztecAddress},
selector::compute_selector,
};
use dep::authwit::{
auth::{
assert_current_call_valid_authwit,
assert_current_call_valid_authwit_public,
},
};
use crate::types::{
transparent_note::{TransparentNote, TransparentNoteMethods, TRANSPARENT_NOTE_LEN},
token_note::{TokenNote, TokenNoteMethods, TOKEN_NOTE_LEN},
balances_map::{BalancesMap},
safe_u120_serialization::{SafeU120SerializationMethods, SAFE_U120_SERIALIZED_LEN}
};
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L2-L51
We are importing the Option type, items from the value_note
library to help manage private value storage, note utilities, context (for managing private and public execution contexts), state_vars
for helping manage state, types
for data manipulation and oracle
for help passing data from the private to public execution context. We also import the auth
library to handle token authorizations from Account Contracts. Check out the Account Contract with AuthWitness here.
SafeU120 is a library to do safe math operations on unsigned integers that protects against overflows and underflows.
For more detail on execution contexts, see Contract Communication.
We are also importing types from a types.nr
file. The main thing to note from this types file 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 owner
, 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 under the hood. Handling UTXOs is largely abstracted away from developers, but there are some unique things for developers to be aware of when creating and managing private state in an Aztec contract. See State Variables 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:
struct Storage {
admin: PublicState<AztecAddress, AZTEC_ADDRESS_SERIALIZED_LEN>,
minters: Map<PublicState<bool, BOOL_SERIALIZED_LEN>>,
balances: BalancesMap,
total_supply: PublicState<SafeU120, SAFE_U120_SERIALIZED_LEN>,
pending_shields: Set<TransparentNote, TRANSPARENT_NOTE_LEN>,
public_balances: Map<PublicState<SafeU120, SAFE_U120_SERIALIZED_LEN>>,
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L53-L70
Reading through the storage variables:
admin
a single Field value stored in public state.FIELD_SERIALIZED_LEN
indicates the length of the variable, which is 1 in this case because it's a single Field element. AField
is basically an unsigned integer with a maximum value determined by the underlying cryptographic curve.minters
is a mapping of Fields 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 aSet
ofValueNote
s. The balance is the sum of all of an account'sValueNote
s.total_supply
is a Field value stored in public state and represents the total number of tokens minted.pending_shields
is aSet
ofTransparentNote
s stored in private state. What is stored publicly is a set of commitments toTransparentNote
s.public_balances
is a mapping field elements in public state and represents the publicly viewable balances of accounts.
You can read more about it here.
Initializing Storage
Once we have Storage defined, we need to specify how to initialize it. The init
method creates and initializes an instance of Storage
. We define an initialization method for each of the storage variables defined above. Storage initialization is generic and can largely be reused for similar types, across different contracts, but it is important to note that each storage variable specifies it's storage slot, starting at 1.
Also, the public storage variables define the type that they store by passing the methods by which they are serialized. Because all PublicState
in this contract is storing Field elements, each storage variable takes FieldSerializationMethods
.
impl Storage {
fn init(context: Context) -> pub Self {
Storage {
admin: PublicState::new(
context,
1,
AztecAddressSerializationMethods,
),
minters: Map::new(
context,
2,
|context, slot| {
PublicState::new(
context,
slot,
BoolSerializationMethods,
)
},
),
balances: BalancesMap::new(context, 3),
total_supply: PublicState::new(
context,
4,
SafeU120SerializationMethods,
),
pending_shields: Set::new(context, 5, TransparentNoteMethods),
public_balances: Map::new(
context,
6,
|context, slot| {
PublicState::new(
context,
slot,
SafeU120SerializationMethods,
)
},
),
}
}
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L72-L121
Functions
Copy and paste the body of each function into the appropriate place in your project if you are following along.
Constructor
In the source code, the constructor logic is commented out due to some limitations of the current state of the development.
#[aztec(private)]
fn constructor(admin: AztecAddress) {
let selector = compute_selector("_initialize((Field))");
context.call_public_function(context.this_address(), selector, [admin.address]);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L123-L129
The constructor is a private function. There isn't any private state to set up in this function, but there is public state to set up. The context
is a global variable that is available to private and public functions, but the available methods differ based on the context. You can see the implementation details here. The context.call_public_function
allows a private function to call a public function on any contract. In this case, the constructor is passing the msg_sender
as the argument to the _initialize
function, which is also defined in this contract.
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(AztecAddress::new(context.msg_sender())), "caller is not admin");
storage.admin.write(new_admin);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L131-L139
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(AztecAddress::new(context.msg_sender())), "caller is not admin");
storage.admin.write(new_admin);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L131-L139
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(AztecAddress::new(context.msg_sender())), "caller is not admin");
storage.minters.at(minter.address).write(approve);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L141-L151
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 SafeU120
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 = SafeU120::new(amount);
let new_balance = storage.public_balances.at(to.address).read().add(amount);
let supply = storage.total_supply.read().add(amount);
storage.public_balances.at(to.address).write(new_balance);
storage.total_supply.write(supply);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L153-L166
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 Set
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(SafeU120::new(amount));
storage.total_supply.write(supply);
pending_shields.insert_from_public(&mut note);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L168-L181
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.address != 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 = SafeU120::new(amount);
let from_balance = storage.public_balances.at(from.address).read().sub(amount);
let pending_shields = storage.pending_shields;
let mut note = TransparentNote::new(amount.value as Field, secret_hash);
storage.public_balances.at(from.address).write(from_balance);
pending_shields.insert_from_public(&mut note);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L183-L202
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 using the SafeU120
library.
#[aztec(public)]
fn transfer_public(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) {
if (from.address != context.msg_sender()) {
assert_current_call_valid_authwit_public(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
let amount = SafeU120::new(amount);
let from_balance = storage.public_balances.at(from.address).read().sub(amount);
storage.public_balances.at(from.address).write(from_balance);
let to_balance = storage.public_balances.at(to.address).read().add(amount);
storage.public_balances.at(to.address).write(to_balance);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L204-L220
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 using the SafeU120
library.
#[aztec(public)]
fn burn_public(from: AztecAddress, amount: Field, nonce: Field) {
if (from.address != context.msg_sender()) {
assert_current_call_valid_authwit_public(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
let amount = SafeU120::new(amount);
let from_balance = storage.public_balances.at(from.address).read().sub(amount);
storage.public_balances.at(from.address).write(from_balance);
let new_supply = storage.total_supply.read().sub(amount);
storage.total_supply.write(new_supply);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L222-L240
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 any Aztec account as a ValueNote
in private balances
.
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 options = NoteGetterOptions::new().select(0, amount).select(1, secret_hash).set_limit(1);
let notes = pending_shields.get_notes(options);
let note = notes[0].unwrap_unchecked();
// Remove the note from the pending shields set
pending_shields.remove(note);
// Add the token note to user's balances set
storage.balances.at(to).add(SafeU120::new(amount));
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L242-L258
unshield
This private function enables un-shielding of private ValueNote
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.address != context.msg_sender()) {
assert_current_call_valid_authwit(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
storage.balances.at(from).sub(SafeU120::new(amount));
let selector = compute_selector("_increase_public_balance((Field),Field)");
let _void = context.call_public_function(context.this_address(), selector, [to.address, amount]);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L260-L274
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(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) {
if (from.address != context.msg_sender()) {
assert_current_call_valid_authwit(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
let amount = SafeU120::new(amount);
storage.balances.at(from).sub(amount);
storage.balances.at(to).add(amount);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L276-L293
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.address != context.msg_sender()) {
assert_current_call_valid_authwit(&mut context, from);
} else {
assert(nonce == 0, "invalid nonce");
}
storage.balances.at(from).sub(SafeU120::new(amount));
let selector = compute_selector("_reduce_total_supply(Field)");
let _void = context.call_public_function(context.this_address(), selector, [amount]);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L295-L309
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.
_initialize
This function is called via the constructor.
This function sets the creator of the contract (passed as msg_sender
from the constructor) as the admin and makes them a minter.
#[aztec(public)]
internal fn _initialize(new_admin: AztecAddress) {
assert(new_admin.address != 0, "invalid admin");
storage.admin.write(new_admin);
storage.minters.at(new_admin.address).write(true);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L311-L318
_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)]
internal fn _increase_public_balance(to: AztecAddress, amount: Field) {
let new_balance = storage.public_balances.at(to.address).read().add(SafeU120::new(amount));
storage.public_balances.at(to.address).write(new_balance);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L322-L328
_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)]
internal fn _reduce_total_supply(amount: Field) {
// Only to be called from burn.
let new_supply = storage.total_supply.read().sub(SafeU120::new(amount));
storage.total_supply.write(new_supply);
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L330-L337
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.
admin
A getter function for reading the public admin
value.
unconstrained fn admin() -> Field {
storage.admin.read().address
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L341-L345
is_minter
A getter function for checking the value of associated with a minter
in the public minters
mapping.
unconstrained fn is_minter(minter: AztecAddress) -> bool {
storage.minters.at(minter.address).read()
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L347-L351
total_supply
A getter function for checking the token total_supply
.
unconstrained fn total_supply() -> u120 {
storage.total_supply.read().value
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L353-L357
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 access to the owner
s decryption keys in order to decrypt their notes.
unconstrained fn balance_of_private(owner: AztecAddress) -> u120 {
storage.balances.at(owner).balance_of().value
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L359-L363
balance_of_public
A getter function for checking the public balance of the provided Aztec account.
unconstrained fn balance_of_public(owner: AztecAddress) -> u120 {
storage.public_balances.at(owner.address).read().value
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L365-L369
compute_note_hash_and_nullifier
A getter function to compute the note hash and nullifier for notes in the contract's storage.
This must be included in every contract because it depends on the storage slots, which are defined when we set up storage.
// Computes note hash and nullifier.
// Note 1: Needs to be defined by every contract producing logs.
// Note 2: Having it in all the contracts gives us the ability to compute the note hash and nullifier differently for different kind of notes.
unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, serialized_note: [Field; TOKEN_NOTE_LEN]) -> [Field; 4] {
let note_header = NoteHeader::new(contract_address, nonce, storage_slot);
if (storage_slot == 5) {
note_utils::compute_note_hash_and_nullifier(TransparentNoteMethods, note_header, serialized_note)
} else {
note_utils::compute_note_hash_and_nullifier(TokenNoteMethods, note_header, serialized_note)
}
}
Source code: /yarn-project/noir-contracts/src/contracts/token_contract/src/main.nr#L374-L386
If your contract works with storage (has Storage struct defined), you MUST include a compute_note_hash_and_nullifier
function.
If you don't yet have any private state variables defined put there a placeholder function:
unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, serialized_note: [Field; 0]) -> [Field; 4] {
[0, 0, 0, 0]
}
Source code: /yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr#L172-L176
Compiling
Now that the contract is complete, you should be able to compile it with the aztec-cli
. See the CLI page for instructions on setting it up.
aztec-cli compile /path/to/token_contracts_folder
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.