Private Token Contract
The Privacy Challenge: Mental Health Benefits at Giggle
Giggle (a fictional tech company) wants to support their employees' mental health by providing BOB tokens that can be spent at Bob's Psychology Clinic. However, employees have a crucial requirement: complete privacy. They don't want Giggle to know:
- How many BOB tokens they've actually used
- When they're using mental health services
- Their therapy patterns or frequency
In this tutorial, we'll build a token contract that allows Giggle to mint BOB tokens for employees while ensuring complete privacy in how those tokens are spent.
Prerequisites
This is an intermediate tutorial that assumes you have:
- Completed the Counter Contract tutorial
- A Running Aztec local network (see the Counter tutorial for setup)
- Basic understanding of Aztec.nr syntax and structure
- Aztec toolchain installed (
bash -i <(curl -s https://install.aztec.network/4.0.0-nightly.20260121/))
If you haven't completed the Counter Contract tutorial, please do so first as we'll skip the basic setup steps covered there.
What We're Building
We'll create BOB tokens with:
- Public and Private minting: Giggle can mint tokens in private or public
- Public and Private transfers: Employees can spend tokens at Bob's clinic with full privacy
Project Setup
Let's create a simple yarn + aztec.nr project:
mkdir bob_token_contract
cd bob_token_contract
yarn init
# This is to ensure yarn uses node_modules instead of pnp for dependency installation
yarn config set nodeLinker node-modules
yarn add @aztec/aztec.js@v4.0.0-nightly.20260121 @aztec/accounts@v4.0.0-nightly.20260121 @aztec/test-wallet@v4.0.0-nightly.20260121 @aztec/kv-store@v4.0.0-nightly.20260121
aztec init
Contract structure
We have a messy, but working structure. In src/main.nr we even have a proto-contract. Let's replace it with a simple starting point:
use aztec::macros::aztec;
#[aztec]
pub contract BobToken {
// We'll build the mental health token here
}
The #[aztec] macro transforms our contract code to work with Aztec's privacy protocol.
Let's import the Aztec.nr library by adding it to our dependencies in Nargo.toml:
[package]
name = "bob_token_contract"
type = "contract"
[dependencies]
aztec = { git = "https://github.com/AztecProtocol/aztec-nr/", tag = "v4.0.0-nightly.20260121", directory = "aztec" }
Since we're here, let's import more specific stuff from this library:
#[aztec]
pub contract BobToken {
use aztec::{
macros::{functions::{external, initializer, only_self}, storage::storage},
messages::message_delivery::MessageDelivery,
protocol_types::address::AztecAddress,
state_vars::{Map, Owned},
state_vars::public_mutable::PublicMutable,
};
}
These are the different macros we need to define the visibility of functions, and some handy types and functions.
Building the Mental Health Token System
The Privacy Architecture
Before we start coding, let's understand how privacy works in our mental health token system:
- Public Layer: Giggle mints tokens publicly - transparent and auditable
- Private Layer: Employees transfer and spend tokens privately - completely confidential
- Cross-layer Transfer: Employees can move tokens between public and private domains as needed
This architecture ensures that while the initial allocation is transparent (important for corporate governance), the actual usage remains completely private.
In Aztec, private state uses a UTXO model with "notes" - think of them as encrypted receipts that only the owner can decrypt and spend. When an employee receives BOB tokens privately, they get encrypted notes that only they can see and use.
Let's start building! Remember to import types as needed - your IDE's Noir extension can help with auto-imports.
Part 1: Public Minting for Transparency
Let's start with the public components that Giggle will use to mint and track initial token allocations.
Setting Up Storage
First, define the storage for our BOB tokens:
#[storage]
struct Storage<Context> {
// Giggle's admin address
owner: PublicMutable<AztecAddress, Context>,
// Public balances - visible for transparency
public_balances: Map<AztecAddress, PublicMutable<u64, Context>, Context>,
}
This storage structure allows:
owner: Stores Giggle's admin address (who can mint tokens)public_balances: Tracks public token balances (employees can verify their allocations)
While employees want privacy when spending, having public balances during minting allows:
- Employees to verify they received their mental health benefits
- Auditors to confirm fair distribution
- Transparency in the allocation process
Initializing Giggle as Owner
When deploying the contract, we need to set Giggle as the owner:
#[initializer]
#[external("public")]
fn setup() {
// Giggle becomes the owner who can mint mental health tokens
self.storage.owner.write(self.msg_sender());
}
Source code: docs/examples/contracts/bob_token_contract/src/main.nr#L33-L40
The #[initializer] decorator ensures this runs once during deployment. Only Giggle's address will have the power to mint new BOB tokens for employees.
Minting BOB Tokens for Employees
Giggle needs a way to allocate mental health tokens to employees:
#[external("public")]
fn mint_public(employee: AztecAddress, amount: u64) {
// Only Giggle can mint tokens
assert_eq(self.msg_sender(), self.storage.owner.read(), "Only Giggle can mint BOB tokens");
// Add tokens to employee's public balance
let current_balance = self.storage.public_balances.at(employee).read();
self.storage.public_balances.at(employee).write(current_balance + amount);
}
Source code: docs/examples/contracts/bob_token_contract/src/main.nr#L42-L52
This public minting function:
- Verifies that only Giggle (the owner) is calling
- Transparently adds tokens to the employee's public balance
- Creates an auditable record of the allocation
Imagine Giggle allocating 100 BOB tokens to each employee at the start of the year. This public minting ensures employees can verify they received their benefits, while their actual usage remains private.
Public Transfers (Optional Transparency)
While most transfers will be private, we'll add public transfers for cases where transparency is desired:
#[external("public")]
fn transfer_public(to: AztecAddress, amount: u64) {
let sender = self.msg_sender();
let sender_balance = self.storage.public_balances.at(sender).read();
assert(sender_balance >= amount, "Insufficient BOB tokens");
// Deduct from sender
self.storage.public_balances.at(sender).write(sender_balance - amount);
// Add to recipient
let recipient_balance = self.storage.public_balances.at(to).read();
self.storage.public_balances.at(to).write(recipient_balance + amount);
}
Source code: docs/examples/contracts/bob_token_contract/src/main.nr#L54-L68
This might be used when:
- An employee transfers tokens to a colleague who's comfortable with transparency
- Bob's clinic makes a public refund
- Any scenario where privacy isn't required
Admin Transfer (Future-Proofing)
In case Giggle's mental health program administration changes:
#[external("public")]
fn transfer_ownership(new_owner: AztecAddress) {
assert_eq(self.msg_sender(), self.storage.owner.read(), "Only current admin can transfer ownership");
self.storage.owner.write(new_owner);
}
Source code: docs/examples/contracts/bob_token_contract/src/main.nr#L70-L76
Your First Deployment - Let's See It Work
Compile Your Contract
You've written enough code to have a working token! Let's compile and test it:
aztec compile
Generate TypeScript Interface
aztec codegen target --outdir artifacts
You should now have a nice typescript interface in a new artifacts folder. Pretty useful!
Deploy and Test
Create index.ts. We will connect to our running local network and its wallet, then deploy the test accounts and get three wallets out of it. Ensure that your local network is running:
aztec start --local-network
Then we will use the giggleWallet to deploy our contract, mint 100 BOB to Alice, then transfer 10 of those to Bob's Clinic publicly... for now. Let's go:
import { BobTokenContract } from './artifacts/BobToken.js';
import { AztecAddress } from '@aztec/aztec.js/addresses';
import { createAztecNodeClient } from '@aztec/aztec.js/node';
import { getInitialTestAccountsData } from '@aztec/accounts/testing';
import { TestWallet } from '@aztec/test-wallet/server';
import { openTmpStore } from '@aztec/kv-store/lmdb';
async function main() {
// Connect to local network
const node = createAztecNodeClient('http://localhost:8080');
const wallet = await TestWallet.create(node);
const [giggleWalletData, aliceWalletData, bobClinicWalletData] = await getInitialTestAccountsData();
const giggleAccount = await wallet.createSchnorrAccount(giggleWalletData.secret, giggleWalletData.salt);
const aliceAccount = await wallet.createSchnorrAccount(aliceWalletData.secret, aliceWalletData.salt);
const bobClinicAccount = await wallet.createSchnorrAccount(bobClinicWalletData.secret, bobClinicWalletData.salt);
const giggleAddress = (await giggleAccount.getAccount()).getAddress();
const aliceAddress = (await aliceAccount.getAccount()).getAddress();
const bobClinicAddress = (await bobClinicAccount.getAccount()).getAddress();
const bobToken = await BobTokenContract
.deploy(
wallet,
)
.send({ from: giggleAddress })
.deployed();
await bobToken.methods
.mint_public(aliceAddress, 100n)
.send({ from: giggleAddress })
.wait();
await bobToken.methods
.transfer_public(bobClinicAddress, 10n)
.send({ from: aliceAddress })
.wait();
}
main().catch(console.error);
Run your test:
npx tsx index.ts
What's this tsx dark magic? Well, it just compiles and runs typescript using reasonable defaults. Pretty cool for small snippets like this!