Contract Deployment
To add contracts to your application, we'll start by creating a new aztec-nargo
project. We'll then compile the contracts, and write a simple script to deploy them to our Sandbox.
Follow the instructions here to install aztec-nargo
if you haven't done so already.
Initialize Aztec project
Create a new contracts
folder, and from there, initialize a new project called token
:
mkdir contracts && cd contracts
aztec-nargo new --contract token
Then, open the contracts/token/Nargo.toml
configuration file, and add the aztec.nr
and value_note
libraries as dependencies:
[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.62.0", directory="noir-projects/aztec-nr/aztec" }
authwit = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.62.0", directory="noir-projects/aztec-nr/authwit"}
compressed_string = {git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.62.0", directory="noir-projects/aztec-nr/compressed-string"}
Last, copy-paste the code from the Token
contract into contracts/token/main.nr
:
mod types;
mod test;
use dep::aztec::macros::aztec;
// 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.
#[aztec]
contract Token {
// Libs
use std::meta::derive;
use dep::compressed_string::FieldCompressedString;
use dep::aztec::{
context::{PrivateCallInterface, PrivateContext},
encrypted_logs::{
encrypted_event_emission::encode_and_encrypt_event_unconstrained,
encrypted_note_emission::{
encode_and_encrypt_note, encode_and_encrypt_note_unconstrained,
},
},
hash::compute_secret_hash,
keys::getters::get_public_keys,
macros::{
events::event,
functions::{initializer, internal, private, public, view},
storage::storage,
},
oracle::random::random,
prelude::{
AztecAddress, FunctionSelector, Map, NoteGetterOptions, PrivateSet, PublicContext,
PublicMutable, SharedImmutable,
},
protocol_types::{point::Point, traits::Serialize},
utils::comparison::Comparator,
};
use dep::uint_note::uint_note::UintNote;
use dep::authwit::auth::{
assert_current_call_valid_authwit, assert_current_call_valid_authwit_public,
compute_authwit_nullifier,
};
use crate::types::{balance_set::BalanceSet, transparent_note::TransparentNote};
// In the first transfer iteration we are computing a lot of additional information (validating inputs, retrieving
// keys, etc.), so the gate count is already relatively high. We therefore only read a few notes to keep the happy
// case with few constraints.
global INITIAL_TRANSFER_CALL_MAX_NOTES: u32 = 2;
// All the recursive call does is nullify notes, meaning the gate count is low, but it is all constant overhead. We
// therefore read more notes than in the base case to increase the efficiency of the overhead, since this results in
// an overall small circuit regardless.
global RECURSIVE_TRANSFER_CALL_MAX_NOTES: u32 = 8;
#[derive(Serialize)]
#[event]
struct Transfer {
from: AztecAddress,
to: AztecAddress,
amount: Field,
}
#[storage]
struct Storage<Context> {
admin: PublicMutable<AztecAddress, Context>,
minters: Map<AztecAddress, PublicMutable<bool, Context>, Context>,
balances: Map<AztecAddress, BalanceSet<Context>, Context>,
total_supply: PublicMutable<U128, Context>,
pending_shields: PrivateSet<TransparentNote, Context>,
public_balances: Map<AztecAddress, PublicMutable<U128, Context>, Context>,
symbol: SharedImmutable<FieldCompressedString, Context>,
name: SharedImmutable<FieldCompressedString, Context>,
decimals: SharedImmutable<u8, Context>,
}
#[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);
}
#[public]
fn set_admin(new_admin: AztecAddress) {
assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin");
storage.admin.write(new_admin);
}
#[public]
#[view]
fn public_get_name() -> FieldCompressedString {
storage.name.read_public()
}
#[private]
#[view]
fn private_get_name() -> FieldCompressedString {
storage.name.read_private()
}
#[public]
#[view]
fn public_get_symbol() -> pub FieldCompressedString {
storage.symbol.read_public()
}
#[private]
#[view]
fn private_get_symbol() -> pub FieldCompressedString {
storage.symbol.read_private()
}
#[public]
#[view]
fn public_get_decimals() -> pub u8 {
storage.decimals.read_public()
}
#[private]
#[view]
fn private_get_decimals() -> pub u8 {
storage.decimals.read_private()
}
#[public]
#[view]
fn get_admin() -> Field {
storage.admin.read().to_field()
}
#[public]
#[view]
fn is_minter(minter: AztecAddress) -> bool {
storage.minters.at(minter).read()
}
#[public]
#[view]
fn total_supply() -> Field {
storage.total_supply.read().to_integer()
}
#[public]
#[view]
fn balance_of_public(owner: AztecAddress) -> Field {
storage.public_balances.at(owner).read().to_integer()
}
#[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);
}
#[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);
}
// TODO(benesjan): To be nuked in a followup PR.
#[public]
fn mint_private_old(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);
}
#[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);
}
#[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);
}
#[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);
}
#[private]
fn redeem_shield(to: AztecAddress, amount: Field, secret: Field) {
let secret_hash = compute_secret_hash(secret);
// Pop 1 note (set_limit(1)) which has an amount stored in a field with index 0 (select(0, amount)) and
// a secret_hash stored in a field with index 1 (select(1, secret_hash)).
let mut options = NoteGetterOptions::new();
options = options
.select(TransparentNote::properties().amount, Comparator.EQ, amount)
.select(TransparentNote::properties().secret_hash, Comparator.EQ, secret_hash)
.set_limit(1);
let notes = storage.pending_shields.pop_notes(options);
assert(notes.len() == 1, "note not popped");
// 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();
let from_ovpk_m = get_public_keys(from).ovpk_m;
storage.balances.at(to).add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(
&mut context,
from_ovpk_m,
to,
context.msg_sender(),
));
}
#[private]
fn transfer_to_public(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");
}
let from_ovpk_m = get_public_keys(from).ovpk_m;
// TODO: constrain encryption below - we are using unconstrained here only becuase of the following Noir issue
// https://github.com/noir-lang/noir/issues/5771
storage.balances.at(from).sub(from, U128::from_integer(amount)).emit(
encode_and_encrypt_note_unconstrained(&mut context, from_ovpk_m, from, from),
);
Token::at(context.this_address())._increase_public_balance(to, amount).enqueue(&mut context);
}
#[private]
fn transfer(to: AztecAddress, amount: Field) {
let from = context.msg_sender();
let from_ovpk_m = get_public_keys(from).ovpk_m;
let amount = U128::from_integer(amount);
// We reduce `from`'s balance by amount by recursively removing notes over potentially multiple calls. This
// method keeps the gate count for each individual call low - reading too many notes at once could result in
// circuits in which proving is not feasible.
// Since the sum of the amounts in the notes we nullified was potentially larger than amount, we create a new
// note for `from` with the change amount, e.g. if `amount` is 10 and two notes are nullified with amounts 8 and
// 5, then the change will be 3 (since 8 + 5 - 10 = 3).
let change = subtract_balance(
&mut context,
storage,
from,
amount,
INITIAL_TRANSFER_CALL_MAX_NOTES,
);
storage.balances.at(from).add(from, change).emit(encode_and_encrypt_note_unconstrained(
&mut context,
from_ovpk_m,
from,
from,
));
storage.balances.at(to).add(to, amount).emit(encode_and_encrypt_note_unconstrained(
&mut context,
from_ovpk_m,
to,
from,
));
// We don't constrain encryption of the note log in `transfer` (unlike in `transfer_from`) because the transfer
// function is only designed to be used in situations where the event is not strictly necessary (e.g. payment to
// another person where the payment is considered to be successful when the other party successfully decrypts a
// note).
Transfer { from, to, amount: amount.to_field() }.emit(
encode_and_encrypt_event_unconstrained(&mut context, from_ovpk_m, to, from),
);
}
#[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);
// Failing to subtract any amount means that the owner was unable to produce more notes that could be nullified.
// We could in some cases fail early inside try_sub if we detected that fewer notes than the maximum were
// returned and we were still unable to reach the target amount, but that'd make the code more complicated, and
// optimizing for the failure scenario is not as important.
assert(subtracted > U128::from_integer(0), "Balance too low");
if subtracted >= amount {
// We have achieved our goal of nullifying notes that add up to more than amount, so we return the change
subtracted - amount
} else {
// try_sub failed to nullify enough notes to reach the target amount, so we compute the amount remaining
// and try again.
let remaining = amount - subtracted;
compute_recurse_subtract_balance_call(*context, account, remaining).call(context)
}
}
// TODO(#7729): apply no_predicates to the contract interface method directly instead of having to use a wrapper
// like we do here.
#[no_predicates]
#[contract_library_method]
fn compute_recurse_subtract_balance_call(
context: PrivateContext,
account: AztecAddress,
remaining: U128,
) -> PrivateCallInterface<25, U128> {
Token::at(context.this_address())._recurse_subtract_balance(account, remaining.to_field())
}
// TODO(#7728): even though the amount should be a U128, we can't have that type in a contract interface due to
// serialization issues.
#[internal]
#[private]
fn _recurse_subtract_balance(account: AztecAddress, amount: Field) -> U128 {
subtract_balance(
&mut context,
storage,
account,
U128::from_integer(amount),
RECURSIVE_TRANSFER_CALL_MAX_NOTES,
)
}
/**
* Cancel a private authentication witness.
* @param inner_hash The inner hash of the authwit to cancel.
*/
#[private]
fn cancel_authwit(inner_hash: Field) {
let on_behalf_of = context.msg_sender();
let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash);
context.push_nullifier(nullifier);
}
#[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");
}
let from_ovpk_m = get_public_keys(from).ovpk_m;
let amount = U128::from_integer(amount);
// TODO: constrain encryption below - we are using unconstrained here only becuase of the following Noir issue
// https://github.com/noir-lang/noir/issues/5771
storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note_unconstrained(
&mut context,
from_ovpk_m,
from,
from,
));
// TODO: constrain encryption below - we are using unconstrained here only becuase of the following Noir issue
// https://github.com/noir-lang/noir/issues/5771
storage.balances.at(to).add(to, amount).emit(encode_and_encrypt_note_unconstrained(
&mut context,
from_ovpk_m,
to,
from,
));
}
#[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");
}
let from_ovpk_m = get_public_keys(from).ovpk_m;
// TODO: constrain encryption below - we are using unconstrained here only becuase of the following Noir issue
// https://github.com/noir-lang/noir/issues/5771
storage.balances.at(from).sub(from, U128::from_integer(amount)).emit(
encode_and_encrypt_note_unconstrained(&mut context, from_ovpk_m, from, from),
);
Token::at(context.this_address())._reduce_total_supply(amount).enqueue(&mut context);
}
// Transfers token `amount` from public balance of message sender to a private balance of `to`.
#[private]
fn transfer_to_private(to: AztecAddress, amount: Field) {
// We check the minter permissions in the enqueued call as that allows us to avoid the need for `SharedMutable`
// which is less efficient.
let from = context.msg_sender();
let token = Token::at(context.this_address());
// We prepare the transfer.
let hiding_point_slot = _prepare_transfer_to_private(to, &mut context, storage);
// At last we finalize the transfer. Usage of the `unsafe` method here is safe because we set the `from`
// function argument to a message sender, guaranteeing that he can transfer only his own tokens.
token._finalize_transfer_to_private_unsafe(from, amount, hiding_point_slot).enqueue(
&mut context,
);
}
/// Prepares a transfer to a private balance of `to`. The transfer then needs to be
/// finalized by calling `finalize_transfer_to_private`. Returns a hiding point slot.
#[private]
fn prepare_transfer_to_private(to: AztecAddress) -> Field {
_prepare_transfer_to_private(to, &mut context, storage)
}
/// This function exists separately from `prepare_transfer_to_private` solely as an optimization as it allows
/// us to have it inlined in the `transfer_to_private` function which results in one less kernel iteration.
///
/// TODO(#9180): Consider adding macro support for functions callable both as an entrypoint and as an internal
/// function.
#[contract_library_method]
fn _prepare_transfer_to_private(
to: AztecAddress,
context: &mut PrivateContext,
storage: Storage<&mut PrivateContext>,
) -> Field {
let to_note_slot = storage.balances.at(to).set.storage_slot;
// We create a setup payload with unpopulated/zero `amount` for 'to'
// TODO(#7775): Manually fetching the randomness here is not great. If we decide to include randomness in all
// notes we could just inject it in macros.
let note_randomness = unsafe { random() };
let note_setup_payload = UintNote::setup_payload().new(to, note_randomness, to_note_slot);
// We set the ovpk to the message sender's ovpk and we encrypt the log.
let from_ovpk = get_public_keys(context.msg_sender()).ovpk_m;
let setup_log =
note_setup_payload.encrypt_log(context, from_ovpk, to, context.msg_sender());
// Using the x-coordinate as a hiding point slot is safe against someone else interfering with it because
// we have a guarantee that the public functions of the transaction are executed right after the private ones
// and for this reason the protocol guarantees that nobody can front-run us in consuming the hiding point.
// This guarantee would break if `finalize_transfer_to_private` was not called in the same transaction. This
// however is not the flow we are currently concerned with. To support the multi-transaction flow we could
// introduce a `from` function argument, hash the x-coordinate with it and then repeat the hashing in
// `finalize_transfer_to_private`.
// We can also be sure that the `hiding_point_slot` will not overwrite any other value in the storage because
// in our state variables we derive slots using a different hash function from multi scalar multiplication
// (MSM).
let hiding_point_slot = note_setup_payload.hiding_point.x;
// We don't need to perform a check that the value overwritten by `_store_point_in_transient_storage_unsafe`
// is zero because the slot is the x-coordinate of the hiding point and hence we could only overwrite
// the value in the slot with the same value. This makes usage of the `unsafe` method safe.
Token::at(context.this_address())
._store_payload_in_transient_storage_unsafe(
hiding_point_slot,
note_setup_payload.hiding_point,
setup_log,
)
.enqueue(context);
hiding_point_slot
}
/// Finalizes a transfer of token `amount` from public balance of `from` to a private balance of `to`.
/// The transfer must be prepared by calling `prepare_transfer_to_private` first and the resulting
/// `hiding_point_slot` must be passed as an argument to this function.
#[public]
fn finalize_transfer_to_private(amount: Field, hiding_point_slot: Field) {
let from = context.msg_sender();
_finalize_transfer_to_private(from, amount, hiding_point_slot, &mut context, storage);
}
#[public]
#[internal]
fn _finalize_transfer_to_private_unsafe(
from: AztecAddress,
amount: Field,
hiding_point_slot: Field,
) {
_finalize_transfer_to_private(from, amount, hiding_point_slot, &mut context, storage);
}
#[contract_library_method]
fn _finalize_transfer_to_private(
from: AztecAddress,
amount: Field,
hiding_point_slot: Field,
context: &mut PublicContext,
storage: Storage<&mut PublicContext>,
) {
// TODO(#8271): Type the amount as U128 and nuke the ugly cast
let amount = U128::from_integer(amount);
// First we subtract the `amount` from the public balance of `from`
let from_balance = storage.public_balances.at(from).read().sub(amount);
storage.public_balances.at(from).write(from_balance);
// Then we finalize the partial note with the `amount`
let finalization_payload =
UintNote::finalization_payload().new(context, hiding_point_slot, amount);
// At last we emit the note hash and the final log
finalization_payload.emit();
}
/// Mints token `amount` to a private balance of `to`. Message sender has to have minter permissions (checked
/// in the enqueud call).
#[private]
fn mint_to_private(to: AztecAddress, amount: Field) {
let from = context.msg_sender();
let token = Token::at(context.this_address());
// We prepare the transfer.
let hiding_point_slot = _prepare_transfer_to_private(to, &mut context, storage);
// At last we finalize the mint. Usage of the `unsafe` method here is safe because we set the `from`
// function argument to a message sender, guaranteeing that only a message sender with minter permissions
// can successfully execute the function.
token._finalize_mint_to_private_unsafe(from, amount, hiding_point_slot).enqueue(&mut context);
}
/// Finalizes a mint of token `amount` to a private balance of `to`. The mint must be prepared by calling
/// `prepare_transfer_to_private` first and the resulting
/// `hiding_point_slot` must be passed as an argument to this function.
///
/// Note: This function is only an optimization as it could be replaced by a combination of `mint_public`
/// and `finalize_transfer_to_private`. It is however used very commonly so it makes sense to optimize it
/// (e.g. used during token bridging, in AMM liquidity token etc.).
#[public]
fn finalize_mint_to_private(amount: Field, hiding_point_slot: Field) {
assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter");
_finalize_mint_to_private(amount, hiding_point_slot, &mut context, storage);
}
#[public]
#[internal]
fn _finalize_mint_to_private_unsafe(
from: AztecAddress,
amount: Field,
hiding_point_slot: Field,
) {
// We check the minter permissions as it was not done in `mint_to_private` function.
assert(storage.minters.at(from).read(), "caller is not minter");
_finalize_mint_to_private(amount, hiding_point_slot, &mut context, storage);
}
#[contract_library_method]
fn _finalize_mint_to_private(
amount: Field,
hiding_point_slot: Field,
context: &mut PublicContext,
storage: Storage<&mut PublicContext>,
) {
let amount = U128::from_integer(amount);
// First we increase the total supply by the `amount`
let supply = storage.total_supply.read().add(amount);
storage.total_supply.write(supply);
// Then we finalize the partial note with the `amount`
let finalization_payload =
UintNote::finalization_payload().new(context, hiding_point_slot, amount);
// At last we emit the note hash and the final log
finalization_payload.emit();
}
/// We need to use different randomness for the user and for the fee payer notes because if the randomness values
/// were the same we could fingerprint the user by doing the following:
/// 1) randomness_influence = fee_payer_point - G_npk * fee_payer_npk =
/// = (G_npk * fee_payer_npk + G_rnd * randomness + G_slot * fee_payer_slot)
/// - G_npk * fee_payer_npk - G_slot * fee_payer_slot =
/// = G_rnd * randomness
/// 2) user_fingerprint = user_point - randomness_influence =
/// = (G_npk * user_npk + G_rnd * randomness + G_slot * user_slot) - G_rnd * randomness =
/// = G_npk * user_npk + G_slot * user_slot
/// 3) Then the second time the user would use this fee paying contract we would recover the same fingerprint
/// and link that the 2 transactions were made by the same user. Given that it's expected that only
/// a limited set of fee paying contracts will be used and they will be known, searching for fingerprints
/// by trying different fee payers is a feasible attack.
///
/// Note 1: fee_payer_npk is part of the fee_payer address preimage derivation, and is assumed to be known. So
// if we have a known set of fee payer contract addresses getting fee_payer_npk and fee_payer_slot is
// trivial (slot is derived in a `Map<...>` as a hash of balances map slot and a fee payer address).
/// Note 2: fee_payer_point and user_point above are public information because they are passed as args to
/// the public `complete_refund(...)` function.
#[private]
fn setup_refund(
fee_payer: AztecAddress, // Address of the entity which will receive the fee note.
user: AztecAddress, // A user for which we are setting up the fee refund.
funded_amount: Field, // The amount the user funded the fee payer with (represents fee limit).
) {
// 1. This function is called by fee paying contract (fee_payer) when setting up a refund so we need to support
// the authwit flow here and check that the user really permitted fee_payer to set up a refund on their behalf.
assert_current_call_valid_authwit(&mut context, user);
// 2. Since user is the logical sender of all the notes we get user's ovpk and use that in all of them.
let user_ovpk = get_public_keys(user).ovpk_m;
// 3. Deduct the funded amount from the user's balance - this is a maximum fee a user is willing to pay
// (called fee limit in aztec spec). The difference between fee limit and the actual tx fee will be refunded
// to the user in the `complete_refund(...)` function.
let change = subtract_balance(
&mut context,
storage,
user,
U128::from_integer(funded_amount),
INITIAL_TRANSFER_CALL_MAX_NOTES,
);
storage.balances.at(user).add(user, change).emit(encode_and_encrypt_note_unconstrained(
&mut context,
user_ovpk,
user,
user,
));
// 4. Now we get the partial payloads
// TODO(#7775): Manually fetching the randomness here is not great. If we decide to include randomness in all
// notes we could just inject it in macros.
let fee_payer_randomness = unsafe { random() };
let user_randomness = unsafe { random() };
let fee_payer_setup_payload = UintNote::setup_payload().new(
fee_payer,
fee_payer_randomness,
storage.balances.at(fee_payer).set.storage_slot,
);
let user_setup_payload = UintNote::setup_payload().new(
user,
user_randomness,
storage.balances.at(user).set.storage_slot,
);
// 5. We get transient storage slots
// Using the x-coordinate as a hiding point slot is safe against someone else interfering with it because
// we have a guarantee that the public functions of the transaction are executed right after the private ones
// and for this reason the protocol guarantees that nobody can front-run us in consuming the hiding point.
// This guarantee would break if `finalize_transfer_to_private` was not called in the same transaction. This
// however is not the flow we are currently concerned with. To support the multi-transaction flow we could
// introduce a `from` function argument, hash the x-coordinate with it and then repeat the hashing in
// `finalize_transfer_to_private`.
// We can also be sure that the `hiding_point_slot` will not overwrite any other value in the storage because
// in our state variables we derive slots using a different hash function from multi scalar multiplication
// (MSM).
let fee_payer_point_slot = fee_payer_setup_payload.hiding_point.x;
let user_point_slot = user_setup_payload.hiding_point.x;
// 6. We compute setup logs
let fee_payer_setup_log =
fee_payer_setup_payload.encrypt_log(&mut context, user_ovpk, fee_payer, fee_payer);
let user_setup_log =
user_setup_payload.encrypt_log(&mut context, user_ovpk, user, fee_payer);
// 7. We store the hiding points an logs in transients storage
Token::at(context.this_address())
._store_payload_in_transient_storage_unsafe(
fee_payer_point_slot,
fee_payer_setup_payload.hiding_point,
fee_payer_setup_log,
)
.enqueue(&mut context);
Token::at(context.this_address())
._store_payload_in_transient_storage_unsafe(
user_point_slot,
user_setup_payload.hiding_point,
user_setup_log,
)
.enqueue(&mut context);
// 8. Set the public teardown function to `complete_refund(...)`. Public teardown is the only time when a public
// function has access to the final transaction fee, which is needed to compute the actual refund amount.
context.set_public_teardown_function(
context.this_address(),
comptime { FunctionSelector::from_signature("complete_refund(Field,Field,Field)") },
[fee_payer_point_slot, user_point_slot, funded_amount],
);
}
// TODO(#9375): Having to define the note log length here is very unfortunate as it's basically impossible for
// users to derive manually. This will however go away once we have a real transient storage since we will not need
// the public call and instead we would do something like `context.transient_storage_write(slot, payload)` and that
// will allow us to use generics and hence user will not need to define it explicitly. We cannot use generics here
// as it is an entrypoint function.
#[public]
#[internal]
fn _store_payload_in_transient_storage_unsafe(
slot: Field,
point: Point,
setup_log: [Field; 15],
) {
context.storage_write(slot, point);
context.storage_write(slot + aztec::protocol_types::point::POINT_LENGTH as Field, setup_log);
}
// TODO(#7728): even though the funded_amount should be a U128, we can't have that type in a contract interface due
// to serialization issues.
#[public]
#[internal]
fn complete_refund(fee_payer_slot: Field, user_slot: Field, funded_amount: Field) {
// TODO(#7728): Remove the next line
let funded_amount = U128::from_integer(funded_amount);
let tx_fee = U128::from_integer(context.transaction_fee());
// 1. We check that user funded the fee payer contract with at least the transaction fee.
// TODO(#7796): we should try to prevent reverts here
assert(funded_amount >= tx_fee, "funded amount not enough to cover tx fee");
// 2. We compute the refund amount as the difference between funded amount and tx fee.
let refund_amount = funded_amount - tx_fee;
// 3. We construct the note finalization payloads with the correct amounts and hiding points to get the note
// hashes and unencrypted logs.
let fee_payer_finalization_payload =
UintNote::finalization_payload().new(&mut context, fee_payer_slot, tx_fee);
let user_finalization_payload =
UintNote::finalization_payload().new(&mut context, user_slot, refund_amount);
// 4. At last we emit the note hashes and the final note logs.
fee_payer_finalization_payload.emit();
user_finalization_payload.emit();
// --> Once the tx is settled user and fee recipient can add the notes to their pixies.
}
/// Internal ///
#[public]
#[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);
}
#[public]
#[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);
}
/// Unconstrained ///
pub(crate) unconstrained fn balance_of_private(owner: AztecAddress) -> pub Field {
storage.balances.at(owner).balance_of().to_field()
}
}
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L1-L841
Helper files
Remove the mod test;
line from contracts/token/src/main.nr
as we will not be using TXE tests in this tutorial.
The Token
contract also requires some helper files. You can view the files here (GitHub link). Copy the types.nr
and the types
folder into contracts/token/src
.
Compile your contract
We'll now use aztec-nargo
to compile.
Now run the following from your contract folder (containing Nargo.toml):
aztec-nargo compile
Deploy your contracts
Let's now write a script for deploying your contracts to the Sandbox. We'll create a Private eXecution Environment (PXE) client, and then use the ContractDeployer
class to deploy our contracts, and store the deployment address to a local JSON file.
Create a new file src/deploy.mjs
. We import the contract artifacts we have generated plus the dependencies we'll need, and then we can deploy the contracts by adding the following code to the src/deploy.mjs
file.
// src/deploy.mjs
import { getInitialTestAccountsWallets } from '@aztec/accounts/testing';
import { Contract, createPXEClient, loadContractArtifact, waitForPXE } from '@aztec/aztec.js';
import TokenContractJson from "../contracts/token/target/token-Token.json" assert { type: "json" };
import { writeFileSync } from 'fs';
const TokenContractArtifact = loadContractArtifact(TokenContractJson);
const { PXE_URL = 'http://localhost:8080' } = process.env;
async function main() {
const pxe = createPXEClient(PXE_URL);
await waitForPXE(pxe);
const [ownerWallet] = await getInitialTestAccountsWallets(pxe);
const ownerAddress = ownerWallet.getAddress();
const token = await Contract.deploy(ownerWallet, TokenContractArtifact, [ownerAddress, 'TokenName', 'TKN', 18])
.send()
.deployed();
console.log(`Token deployed at ${token.address.toString()}`);
const addresses = { token: token.address.toString() };
writeFileSync('addresses.json', JSON.stringify(addresses, null, 2));
}
main().catch((err) => {
console.error(`Error in deployment script: ${err}`);
process.exit(1);
});
Here, we are using the Contract
class with the compiled artifact to send a new deployment transaction. The deployed
method will block execution until the transaction is successfully mined, and return a receipt with the deployed contract address.
Note that the token's constructor()
method expects an owner
address to set as the contract admin
. We are using the first account from the Sandbox for this.
If you are using the generated typescript classes, you can drop the generic ContractDeployer
in favor of using the deploy
method of the generated class, which will automatically load the artifact for you and type-check the constructor arguments. See the How to deploy a contract page for more info.
Run the snippet above as node src/deploy.mjs
, and you should see the following output, along with a new addresses.json
file in your project root:
Token deployed to 0x2950b0f290422ff86b8ee8b91af4417e1464ddfd9dda26de8af52dac9ea4f869
Next steps
Now that we have our contracts set up, it's time to actually start writing our application that will be interacting with them.