Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0f361bb
WIP
benesjan May 23, 2025
b3517f4
wip
benesjan May 23, 2025
6d71ec1
justification of my laziness
benesjan May 23, 2025
8f3edb8
better comments
benesjan May 23, 2025
a9f4573
linking issue
benesjan May 23, 2025
0060448
Update noir-projects/aztec-nr/uint-note/src/uint_note.nr
benesjan May 23, 2025
1edeb21
not using PartialUintNote for order_id
benesjan May 23, 2025
ae387df
returning order_id
benesjan May 26, 2025
5eaf2e6
moving checks to Config
benesjan May 26, 2025
9c3a5b0
zero_to_one --> token_in_is_zero
benesjan May 26, 2025
01d3dad
Update noir-projects/noir-contracts/contracts/app/orderbook_contract/…
benesjan May 26, 2025
5371330
improved comments
benesjan May 26, 2025
6c5c001
nuking unnecessary comment
benesjan May 26, 2025
46eb3f3
Returning whether an order has been fulfilled from get_order
benesjan May 26, 2025
3d52b57
linking issue
benesjan May 26, 2025
af2da1d
better token names
benesjan May 26, 2025
994a114
Add unit tests for Config and Order modules
benesjan May 26, 2025
3aa3fa5
test fix post rename
benesjan May 26, 2025
b801c42
Refactor token initialization in tests to use global variables for co…
benesjan May 27, 2025
13b203b
Add documentation for get_tokens method to clarify its return values …
benesjan May 27, 2025
88fb65f
Update noir-projects/noir-contracts/contracts/app/orderbook_contract/…
benesjan May 27, 2025
294c94f
Update noir-projects/noir-contracts/contracts/app/orderbook_contract/…
benesjan May 27, 2025
65f6141
Refactor get_tokens test to use validate_input_tokens_and_get_directi…
benesjan May 27, 2025
50e2bdf
comment
benesjan May 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions noir-projects/aztec-nr/uint-note/src/uint_note.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use dep::aztec::{
GENERATOR_INDEX__PARTIAL_NOTE_VALIDITY_COMMITMENT,
},
hash::poseidon2_hash_with_separator,
traits::{Deserialize, Hash, Packable, Serialize, ToField},
traits::{Deserialize, FromField, Hash, Packable, Serialize, ToField},
utils::arrays::array_concat,
},
};
Expand Down Expand Up @@ -207,7 +207,7 @@ impl NoteType for PrivateUintPartialNotePrivateLogContent {
/// slot, but the value field has not yet been set. A partial note can be completed in public with the `complete`
/// function (revealing the value to the public), resulting in a UintNote that can be used like any other one (except
/// of course that its value is known).
#[derive(Packable, Serialize, Deserialize)]
#[derive(Packable, Serialize, Deserialize, Eq)]
pub struct PartialUintNote {
commitment: Field,
}
Expand Down Expand Up @@ -268,6 +268,18 @@ impl PartialUintNote {
}
}

impl ToField for PartialUintNote {
fn to_field(self) -> Field {
self.commitment
}
}

impl FromField for PartialUintNote {
fn from_field(field: Field) -> Self {
Self { commitment: field }
}
}

mod test {
use super::{
PartialUintNote, PrivateUintPartialNotePrivateLogContent, UintNote,
Expand Down
1 change: 1 addition & 0 deletions noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ members = [
"contracts/app/escrow_contract",
"contracts/app/lending_contract",
"contracts/app/nft_contract",
"contracts/app/orderbook_contract",
"contracts/app/price_feed_contract",
"contracts/app/token_blacklist_contract",
"contracts/app/token_bridge_contract",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "orderbook_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { path = "../../../../aztec-nr/aztec" }
token = { path = "../token_contract" }
uint_note = { path = "../../../../aztec-nr/uint-note" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use dep::aztec::protocol_types::{address::AztecAddress, traits::{Deserialize, Packable, Serialize}};
use std::meta::derive;

/// We store the tokens of the DEX in a struct such that to load it from PublicImmutable asserts only a single
/// merkle proof.
#[derive(Deserialize, Eq, Packable, Serialize)]
pub struct Config {
token0: AztecAddress,
token1: AztecAddress,
}

impl Config {
pub fn new(token0: AztecAddress, token1: AztecAddress) -> Self {
assert(!token0.eq(token1), "Tokens must be different");
Self { token0, token1 }
}

pub fn validate_input_tokens_and_get_direction(
self,
bid_token: AztecAddress,
ask_token: AztecAddress,
) -> bool {
assert((bid_token == self.token0) | (bid_token == self.token1), "BID_TOKEN_IS_INVALID");
assert((ask_token == self.token0) | (ask_token == self.token1), "ASK_TOKEN_IS_INVALID");
assert(bid_token != ask_token, "SAME_TOKEN_TRADE");

bid_token == self.token0
}

/// Returns a tuple of (bid_token, ask_token) based on `bid_token_is_zero` param.
pub fn get_tokens(self, bid_token_is_zero: bool) -> (AztecAddress, AztecAddress) {
if bid_token_is_zero {
(self.token0, self.token1)
} else {
(self.token1, self.token0)
}
}
}

mod test {
use crate::config::Config;
use aztec::{prelude::AztecAddress, protocol_types::traits::FromField};

global token0: AztecAddress = AztecAddress::from_field(1);
global token1: AztecAddress = AztecAddress::from_field(2);
global token2: AztecAddress = AztecAddress::from_field(3);

#[test]
unconstrained fn new_config_valid_inputs() {
let _ = Config::new(token0, token1);
}

#[test(should_fail_with = "Tokens must be different")]
unconstrained fn new_config_same_tokens() {
let _ = Config::new(token0, token0);
}

#[test]
unconstrained fn validate_input_tokens_valid() {
let config = Config::new(token0, token1);

// Test token0 to token1 direction
let is_zero = config.validate_input_tokens_and_get_direction(token0, token1);
assert(is_zero);

// Test token1 to token0 direction
let is_zero = config.validate_input_tokens_and_get_direction(token1, token0);
assert(!is_zero);
}

#[test(should_fail_with = "BID_TOKEN_IS_INVALID")]
unconstrained fn validate_input_tokens_invalid_bid() {
let config = Config::new(token0, token1);
let _ = config.validate_input_tokens_and_get_direction(token2, token1);
}

#[test(should_fail_with = "ASK_TOKEN_IS_INVALID")]
unconstrained fn validate_input_tokens_invalid_ask() {
let config = Config::new(token0, token1);
let _ = config.validate_input_tokens_and_get_direction(token0, token2);
}

#[test(should_fail_with = "SAME_TOKEN_TRADE")]
unconstrained fn validate_input_tokens_same_token() {
let config = Config::new(token0, token1);
let _ = config.validate_input_tokens_and_get_direction(token0, token0);
}

#[test]
unconstrained fn get_tokens_correct_order() {
let config = Config::new(token0, token1);

let is_zero = config.validate_input_tokens_and_get_direction(token0, token1);
let (bid, ask) = config.get_tokens(is_zero);
assert(bid == token0);
assert(ask == token1);

let is_zero = config.validate_input_tokens_and_get_direction(token1, token0);
let (bid, ask) = config.get_tokens(is_zero);
assert(bid == token1);
assert(ask == token0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
mod config;
mod order;

use aztec::macros::aztec;

/// ## Overview
/// This contract demonstrates how to implement an **Orderbook** that maintains **public state**
/// while still achieving **identity privacy**. However, it does **not provide function privacy**:
/// - Anyone can observe **what actions** were performed.
/// - All amounts involved are visible, but **who** performed the action remains private.
///
/// **Note:**
/// This is purely a demonstration implemented to test various features of Aztec.nr. The **Aztec team** does not
/// consider this the optimal design for building a DEX.
///
/// ## Reentrancy Guard Considerations
///
/// ### 1. Private Functions:
/// Reentrancy protection is typically necessary if entering an intermediate state that is only valid when
/// the action completes uninterrupted. This follows the **Checks-Effects-Interactions** pattern.
///
/// - In this contract, **private functions** do not introduce intermediate states.
/// - All operations will be fully executed in **public** without needing intermediate checks.
///
/// ### 2. Public Functions:
/// No **reentrancy guard** is required for public functions because:
/// - All public functions are marked as **internal** with a **single callsite** - from a private function.
/// - Public functions **cannot call private functions**, eliminating the risk of reentering into them from private.
/// - Since public functions are internal-only, **external contracts cannot access them**, ensuring no external
/// contract can trigger a reentrant call. This eliminates the following attack vector:
/// `Orderbook.private_fn --> Orderbook.public_fn --> ExternalContract.fn --> Orderbook.public_fn`.
#[aztec]
pub contract Orderbook {
use crate::{config::Config, order::Order};
use aztec::{
event::event_interface::EventInterface,
macros::{
events::event,
functions::{initializer, internal, private, public, utility},
storage::storage,
},
oracle::notes::check_nullifier_exists,
prelude::{AztecAddress, Map, PublicImmutable},
protocol_types::traits::{FromField, Serialize, ToField},
unencrypted_logs::unencrypted_event_emission::encode_event,
};

use token::Token;
use uint_note::uint_note::PartialUintNote;

// The event contains only the `order_id` as the order itself can be retrieved via the `get_order` function.
#[derive(Serialize)]
#[event]
struct OrderCreated {
order_id: Field,
}

#[derive(Serialize)]
#[event]
struct OrderFulfilled {
order_id: Field,
}

#[storage]
struct Storage<Context> {
config: PublicImmutable<Config, Context>,
orders: Map<Field, PublicImmutable<Order, Context>, Context>,
}

#[public]
#[initializer]
fn constructor(token0: AztecAddress, token1: AztecAddress) {
storage.config.initialize(Config::new(token0, token1));
}

/// Privately creates a new order in the orderbook
/// The maker specifies the tokens and amounts they want to trade
#[private]
fn create_order(
bid_token: AztecAddress,
ask_token: AztecAddress,
bid_amount: u128,
ask_amount: u128,
nonce: Field,
) -> Field {
let config = storage.config.read();

// Create the order (this validates the input tokens and amounts).
let order = Order::new(config, bid_amount, ask_amount, bid_token, ask_token);

let maker = context.msg_sender();

// Transfer tokens from maker to the public balance of this contract.
Token::at(bid_token)
.transfer_to_public(maker, context.this_address(), bid_amount, nonce)
.call(&mut context);

// Prepare a partial note that will get completed once the order is fulfilled. Note that only the Orderbook
// contract can complete the partial note.
let maker_partial_note =
Token::at(ask_token).prepare_private_balance_increase(maker, maker).call(&mut context);

// We use the partial note's as the order ID. Because partial notes emit a nullifier when created they are
// unique, and so this guarantees that our order IDs are also unique without having to keep track of past
// ones.
let order_id = maker_partial_note.to_field();

// Store the order in public storage and emit an event.
Orderbook::at(context.this_address())._create_order(order_id, order).enqueue(&mut context);

order_id
}

#[public]
#[internal]
fn _create_order(order_id: Field, order: Order) {
// Note that PublicImmutable can be initialized only once so this is a secondary check that the order is
// unique.
storage.orders.at(order_id).initialize(order);

OrderCreated { order_id }.emit(encode_event(&mut context));
}

/// Privately fulfills an existing order in the orderbook
/// The taker provides the order ID they want to fulfill
#[private]
fn fulfill_order(order_id: Field, nonce: Field) {
let config = storage.config.read();
let order = storage.orders.at(order_id).read();
let taker = context.msg_sender();

// Determine which tokens are being exchanged based on bid_token_is_zero flag
let (bid_token, ask_token) = config.get_tokens(order.bid_token_is_zero);

// TODO(#14362): Once we allow partial note completion in private we can skip the transfer to public here and
// just finalize the maker's partial note straight away here.
// Transfer the ask_amount from taker to the contract
Token::at(ask_token)
.transfer_to_public(taker, context.this_address(), order.ask_amount, nonce)
.call(&mut context);

// Prepare partial note for taker to receive bid_token
let taker_partial_note =
Token::at(bid_token).prepare_private_balance_increase(taker, taker).call(&mut context);

// Nullify the order such that it cannot be fulfilled again. We emit a nullifier instead of deleting the order
// from public storage because we get no refund for resetting public storage to zero and just emitting
// a nullifier is cheaper (1 Field in DA instead of multiple Fields for the order). We use the `order_id`
// itself as the nullifier because this contract does not work with notes and hence there is no risk of
// colliding with a real note nullifier.
context.push_nullifier(order_id);

// Enqueue the fulfillment to finalize both partial notes
Orderbook::at(context.this_address())
._fulfill_order(
order_id,
taker_partial_note,
bid_token,
ask_token,
order.bid_amount,
order.ask_amount,
)
.enqueue(&mut context);
}

#[public]
#[internal]
fn _fulfill_order(
order_id: Field,
taker_partial_note: PartialUintNote,
bid_token: AztecAddress,
ask_token: AztecAddress,
bid_amount: u128,
ask_amount: u128,
) {
// The `order_id` is a serialized form of the maker's partial note.
let maker_partial_note = PartialUintNote::from_field(order_id);

// Finalize transfer of ask_amount of ask_token to maker
Token::at(ask_token).finalize_transfer_to_private(ask_amount, maker_partial_note).call(
&mut context,
);

// Finalize transfer of bid_amount of bid_token to taker
Token::at(bid_token).finalize_transfer_to_private(bid_amount, taker_partial_note).call(
&mut context,
);

OrderFulfilled { order_id }.emit(encode_event(&mut context));
}

/// Returns the order and whether it has been fulfilled.
#[utility]
unconstrained fn get_order(order_id: Field) -> pub (Order, bool) {
let order = storage.orders.at(order_id).read();
let is_fulfilled = check_nullifier_exists(order_id);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have nullifier_exists function on public context but not on utility and private contexts. Shall we add it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure. The way we do this in private is with the prove_nullifier_inclusion fn, which expects a full block header. It'd not touch these too much until we use them more, or get external feedback.


(order, is_fulfilled)
}
}
Loading