Skip to main content
Version: dev

Define Functions

There are several types of functions in Aztec contracts that correspond the the different execution environments in which they run. These include:

  • private functions
  • public functions
  • utility functions
  • view functions
  • internal functions
  • initializer functions
  • contract library methods

Private Functions

Private functions execute client-side on user devices to maintain private of user inputs and execution. Specify a private function in your contract using the #[private] function annotation.

withdraw
// Withdraws balance. Requires that msg.sender is the owner.
#[private]
fn withdraw(token: AztecAddress, amount: u128, recipient: AztecAddress) {
let sender = context.msg_sender();

let note = storage.owner.get_note();
assert(note.get_address() == sender);
Token::at(token).transfer(recipient, amount).call(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/app/escrow_contract/src/main.nr#L32-L44

Public Functions

A public function is executed by the sequencer and has access to a state model that is very similar to that of the EVM and Ethereum. Even though they work in an EVM-like model for public transactions, they are able to write data into private storage that can be consumed later by a private function.

Read more about the concept of public functions here.

Declare a public function in your contract using the #[public] function annotation.

mint
#[public]
fn mint(to: AztecAddress, token_id: Field) {
assert(token_id != 0, "zero token ID not supported");
assert(storage.minters.at(context.msg_sender()).read(), "caller is not a minter");
assert(storage.nft_exists.at(token_id).read() == false, "token already exists");

storage.nft_exists.at(token_id).write(true);

storage.public_owners.at(token_id).write(to);
}
Source code: noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr#L91-L102

Utility Functions

Contract functions marked with #[utility] are used to perform state queries from an off-chain client (from both private and public state!) or to modify local contract-related PXE state (e.g. when processing logs in Aztec.nr), and are never included in any transaction. No guarantees are made on the correctness of the result since the entire execution is unconstrained and heavily reliant on oracle calls. Read more about the concept of utility functions here.

get_private_nfts
#[utility]
unconstrained fn get_private_nfts(
owner: AztecAddress,
page_index: u32,
) -> ([Field; MAX_NOTES_PER_PAGE], bool) {
let offset = page_index * MAX_NOTES_PER_PAGE;
let mut options = NoteViewerOptions::new();
let notes = storage.private_nfts.at(owner).view_notes(options.set_offset(offset));

let mut owned_nft_ids = [0; MAX_NOTES_PER_PAGE];
for i in 0..options.limit {
if i < notes.len() {
owned_nft_ids[i] = notes.get_unchecked(i).get_token_id();
}
}

let page_limit_reached = notes.len() == options.limit;
(owned_nft_ids, page_limit_reached)
}
Source code: noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr#L348-L368

View Functions

The #[view] attribute can be applied to a #[private] or a #[public] function and it guarantees that the function cannot modify any contract state (just like view functions in Solidity). This allows you to read private or public state by calling the function from another contract.

For examples, to get the admin address from the NFT contract, you can use the get_admin function:

admin
#[public]
#[view]
fn get_admin() -> Field {
storage.admin.read().to_field()
}
Source code: noir-projects/noir-contracts/contracts/app/nft_contract/src/main.nr#L128-L134

Internal Functions

Internal functions are functions that are only callable within the same contract. They are not visible to other contracts and cannot be called from outside the contract.

Mark an internal function with the #[internal] attribute.

add_to_tally_public
#[public]
#[internal]
fn add_to_tally_public(candidate: Field) {
assert(storage.vote_ended.read() == false, "Vote has ended"); // assert that vote has not ended
let new_tally = storage.tally.at(candidate).read() + 1;
storage.tally.at(candidate).write(new_tally);
}
Source code: noir-projects/noir-contracts/contracts/app/private_voting_contract/src/main.nr#L58-L66

Initializer Functions

Initializers are regular functions that set an "initialized" flag (a nullifier) for the contract. A contract can only be initialized once, and contract functions can only be called after the contract has been initialized, much like a constructor. However, if a contract defines no initializers, it can be called at any time. Additionally, you can define as many initializer functions in a contract as you want, both private and public.

Annotate with #[initializer]

Define your initializer like so:

#[initializer]
fn constructor(){
// function logic here
}

Aztec supports both public and private initializers. Use the appropriate macro, for example for a private initializer:

#[private]
#[initializer]
fn constructor(){
// function logic here
}

Initializers are commonly used to set an admin, such as this example:

constructor
#[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/app/token_contract/src/main.nr#L82-L95

Here, the initializer is writing to storage. It can also call another function. Learn more about calling functions from functions here.

Multiple initializers

You can set multiple functions as an initializer function simply by annotating each of them with #[initializer]. You can then decide which one to call when you are deploying the contract.

Calling any one of the functions annotated with #[initializer] will mark the contract as initialized.

To see an initializer in action, follow the Counter codealong tutorial.

Contract Library Methods

Contract library methods are functions that are used to implement the logic of a contract and reduce code duplication. When called by another function, they are inlined into the calling function. They are not visible to the outside world and are only callable within the same contract.

For example, the subtract_balance function in the simple token contract:

subtract_balance
#[contract_library_method]
fn subtract_balance(
context: &mut PrivateContext,
storage: Storage<&mut PrivateContext>,
account: AztecAddress,
amount: u128,
max_notes: u32,
) -> u128 {
let subtracted = storage.balances.at(account).try_sub(amount, max_notes);
assert(subtracted > 0 as u128, "Balance too low");
if subtracted >= amount {
subtracted - amount
} else {
let remaining = amount - subtracted;
compute_recurse_subtract_balance_call(*context, account, remaining).call(context)
}
}
Source code: noir-projects/noir-contracts/contracts/app/simple_token_contract/src/main.nr#L324-L342