Skip to main content
Version: v3.0.0-nightly.20251231

Counter Contract

In this guide, we will create our first Aztec.nr smart contract. We will build a simple private counter, where you can keep your own private counter - so no one knows what ID you are at or when you increment! This contract will get you started with the basic setup and syntax of Aztec.nr, but doesn't showcase all of the awesome stuff Aztec is capable of.

This tutorial is compatible with the Aztec version v3.0.0-nightly.20251231. Install the correct version with bash -i <(curl -s https://install.aztec.network/3.0.0-nightly.20251231/). Or if you'd like to use a different version, you can find the relevant tutorial by clicking the version dropdown at the top of the page.

Prerequisites

  • You have followed the quickstart
  • Running Aztec local network
  • Installed Noir LSP (optional)

Set up a project

Run this to create a new contract project:

aztec new --contract counter

Your structure should look like this:

.
|-counter
| |-src
| | |-main.nr
| |-Nargo.toml

The file main.nr will soon turn into our smart contract!

Add the following dependencies to Nargo.toml under the autogenerated content:

[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-nr/", tag="v3.0.0-nightly.20251231", directory="aztec" }
balance_set = { git="https://github.com/AztecProtocol/aztec-nr/", tag="v3.0.0-nightly.20251231", directory="balance-set" }

Define the functions

Go to main.nr, and replace the boilerplate code with this contract initialization:

use dep::aztec::macros::aztec;

#[aztec]
pub contract Counter {
}

This defines a contract called Counter.

Imports

We need to define some imports.

Write this inside your contract, ie inside these brackets:

pub contract Counter {
// imports go here!
}
imports
use aztec::{
macros::{functions::{external, initializer}, storage::storage},
messages::message_delivery::MessageDelivery,
oracle::debug_log::debug_log_format,
protocol_types::{address::AztecAddress, traits::ToField},
state_vars::Owned,
};
use balance_set::BalanceSet;
Source code: docs/examples/contracts/counter_contract/src/main.nr#L7-L16
  • macros::{functions::{external, initializer}, storage::storage} Imports the macros needed to define function types (external, initializer) and the storage macro for declaring contract storage structures.

  • messages::message_delivery::MessageDelivery Imports MessageDelivery for specifying how note delivery should be handled (e.g., constrained onchain delivery).

  • oracle::debug_log::debug_log_format Imports a debug logging utility for printing formatted messages during contract execution.

  • protocol_types::{address::AztecAddress, traits::ToField} Brings in AztecAddress (used to identify accounts/contracts) and traits for converting values to field elements, necessary for serialization and formatting inside Aztec.

  • state_vars::Owned Brings in Owned, a wrapper for state variables that have a single owner.

  • use balance_set::BalanceSet Imports BalanceSet from the balance_set dependency, which provides functionality for managing private balances (used for our counter).

Declare storage

Add this below the imports. It declares the storage variables for our contract. We use an Owned state variable wrapping a BalanceSet to manage private balances for each owner.

storage_struct
#[storage]
struct Storage<Context> {
counters: Owned<BalanceSet<Context>, Context>,
}
Source code: docs/examples/contracts/counter_contract/src/main.nr#L18-L23

Keep the counter private

Now we’ve got a mechanism for storing our private state, we can start using it to ensure the privacy of balances.

Let’s create a constructor method to run on deployment that assigns an initial count to a specified owner. This function is called initialize, but behaves like a constructor. It is the #[initializer] decorator that specifies that this function behaves like a constructor. Write this:

constructor
#[initializer]
#[external("private")]
// We can name our initializer anything we want as long as it's marked as aztec(initializer)
fn initialize(headstart: u64, owner: AztecAddress) {
self.storage.counters.at(owner).add(headstart as u128).deliver(
MessageDelivery.CONSTRAINED_ONCHAIN,
);
}
Source code: docs/examples/contracts/counter_contract/src/main.nr#L25-L34

This function accesses the counters from storage. It adds the headstart value to the owner's counter using at().add(), then calls .deliver(MessageDelivery.CONSTRAINED_ONCHAIN) to ensure the note is delivered onchain.

We have annotated this and other functions with #[external("private")] which are ABI macros so the compiler understands it will handle private inputs.

Incrementing our counter

Now let's implement an increment function to increase the counter.

increment
#[external("private")]
fn increment(owner: AztecAddress) {
debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]);
self.storage.counters.at(owner).add(1).deliver(MessageDelivery.CONSTRAINED_ONCHAIN);
}
Source code: docs/examples/contracts/counter_contract/src/main.nr#L36-L42

The increment function works similarly to the initialize function. It logs a debug message, then adds 1 to the owner's counter and delivers the note onchain.

Getting a counter

The last thing we need to implement is a function to retrieve a counter value.

get_counter
#[external("utility")]
unconstrained fn get_counter(owner: AztecAddress) -> pub u128 {
self.storage.counters.at(owner).balance_of()
}
Source code: docs/examples/contracts/counter_contract/src/main.nr#L44-L49

This is a utility function used to obtain the counter value outside of a transaction. We access the owner's balance from the counters storage variable using at(owner), then call balance_of() to retrieve the current count. This yields a private counter that only the owner can decrypt.

Compile

Now we've written a simple Aztec.nr smart contract, we can compile it.

Compile the smart contract

In the ./counter/ directory, run:

aztec compile

This command compiles your Noir contract and creates a target folder with a .json artifact inside.

After compiling, you can generate a TypeScript class using the aztec codegen command.

In the same directory, run this:

aztec codegen -o src/artifacts target

You can now use the artifact and/or the TS class in your Aztec.js!

Next Steps

Optional: Learn more about concepts mentioned here