Skip to content

Commit 59b5076

Browse files
critesjoshclaude
andcommitted
Migrate prediction-market contract to Aztec v3.0.0-devnet.20251212 API
- Add balance_set dependency for BalanceSet storage pattern - Replace Map<AztecAddress, PrivateSet<UintNote>> with Owned<BalanceSet> - Use BalanceSet.add()/sub() with .deliver() for note management - Update partial note creation to use get_storage_slot() from Owned wrapper - Replace #[internal] with #[only_self] for internal public functions - Update storage access from `storage.X` to `self.storage.X` - Update context access from `context` to `self.context` - Use self.enqueue_self for enqueueing public calls - Add pub keyword to unconstrained function return types All 10 Noir unit tests and 9 E2E tests pass. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent efc0ad1 commit 59b5076

File tree

2 files changed

+65
-118
lines changed

2 files changed

+65
-118
lines changed

prediction-market/Nargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ type = "contract"
77
[dependencies]
88
aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.20251212", directory = "noir-projects/aztec-nr/aztec" }
99
uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.20251212", directory = "noir-projects/aztec-nr/uint-note" }
10+
balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.20251212", directory = "noir-projects/aztec-nr/balance-set" }

prediction-market/src/main.nr

Lines changed: 64 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,30 @@
11
mod lib;
22

3-
use dep::aztec::macros::aztec;
3+
use aztec::macros::aztec;
44

55
/// Binary prediction market with full identity privacy using CSMM pricing.
66
/// All balances are private. Public function only sees trade amounts, not who traded.
77
#[aztec]
88
pub contract PredictionMarket {
99
use crate::lib::{calculate_shares_out, calculate_new_price, calculate_price_impact};
10-
use dep::aztec::{
11-
macros::{functions::{external, initializer, internal}, storage::storage},
10+
use aztec::{
11+
macros::{functions::{external, initializer, only_self}, storage::storage},
1212
messages::message_delivery::MessageDelivery,
13-
note::{
14-
constants::MAX_NOTES_PER_PAGE,
15-
note_getter_options::NoteGetterOptions,
16-
note_viewer_options::NoteViewerOptions,
17-
},
18-
protocol_types::{address::AztecAddress, storage::map::derive_storage_slot_in_map},
19-
state_vars::{PublicMutable, Map, PrivateSet},
13+
protocol_types::address::AztecAddress,
14+
state_vars::{PublicMutable, Owned, state_variable::StateVariable},
2015
};
16+
use dep::balance_set::balance_set::BalanceSet;
2117
use dep::uint_note::uint_note::{UintNote, PartialUintNote};
2218

2319
#[storage]
2420
struct Storage<Context> {
25-
collateral_balances: Map<AztecAddress, PrivateSet<UintNote, Context>, Context>,
21+
collateral_balances: Owned<BalanceSet<Context>, Context>,
2622
yes_supply: PublicMutable<u128, Context>,
2723
no_supply: PublicMutable<u128, Context>,
2824
total_liquidity: PublicMutable<u128, Context>,
2925
admin: PublicMutable<AztecAddress, Context>,
30-
yes_balances: Map<AztecAddress, PrivateSet<UintNote, Context>, Context>,
31-
no_balances: Map<AztecAddress, PrivateSet<UintNote, Context>, Context>,
26+
yes_balances: Owned<BalanceSet<Context>, Context>,
27+
no_balances: Owned<BalanceSet<Context>, Context>,
3228
}
3329

3430
pub global PRICE_PRECISION: u128 = 1_000_000;
@@ -40,45 +36,29 @@ pub contract PredictionMarket {
4036
assert(initial_liquidity >= MINIMUM_LIQUIDITY, "INSUFFICIENT_INITIAL_LIQUIDITY");
4137
assert(!admin.is_zero(), "INVALID_ADMIN");
4238

43-
storage.admin.write(admin);
39+
self.storage.admin.write(admin);
4440
let half = initial_liquidity / 2;
45-
storage.yes_supply.write(half);
46-
storage.no_supply.write(half);
47-
storage.total_liquidity.write(initial_liquidity);
41+
self.storage.yes_supply.write(half);
42+
self.storage.no_supply.write(half);
43+
self.storage.total_liquidity.write(initial_liquidity);
4844
}
4945

5046
/// Deposit collateral privately. Creates a private note for the sender.
5147
#[external("private")]
5248
fn deposit(amount: u128) {
5349
assert(amount > 0, "ZERO_AMOUNT");
54-
let sender = context.msg_sender().unwrap();
55-
let note = UintNote::new(amount, sender);
56-
storage.collateral_balances.at(sender).insert(note).emit(sender, MessageDelivery.UNCONSTRAINED_ONCHAIN);
50+
let sender = self.context.msg_sender().unwrap();
51+
self.storage.collateral_balances.at(sender).add(amount)
52+
.deliver(MessageDelivery.UNCONSTRAINED_ONCHAIN);
5753
}
5854

5955
/// Withdraw collateral privately. Consumes notes and returns change.
6056
#[external("private")]
6157
fn withdraw(amount: u128) {
6258
assert(amount > 0, "ZERO_AMOUNT");
63-
let sender = context.msg_sender().unwrap();
64-
65-
let options = NoteGetterOptions::new();
66-
let notes: BoundedVec<UintNote, 16> = storage.collateral_balances.at(sender).pop_notes(options);
67-
68-
let mut total: u128 = 0;
69-
for i in 0..16 {
70-
if i < notes.len() {
71-
total += notes.get_unchecked(i).get_value();
72-
}
73-
}
74-
75-
assert(total >= amount, "INSUFFICIENT_COLLATERAL_BALANCE");
76-
77-
let change = total - amount;
78-
if change > 0 {
79-
let change_note = UintNote::new(change, sender);
80-
storage.collateral_balances.at(sender).insert(change_note).emit(sender, MessageDelivery.UNCONSTRAINED_ONCHAIN);
81-
}
59+
let sender = self.context.msg_sender().unwrap();
60+
self.storage.collateral_balances.at(sender).sub(amount)
61+
.deliver(MessageDelivery.UNCONSTRAINED_ONCHAIN);
8262
}
8363

8464
/// Buy YES or NO shares with full identity privacy.
@@ -87,51 +67,33 @@ pub contract PredictionMarket {
8767
fn buy_outcome(is_yes: bool, collateral_amount: u128, min_shares_out: u128) {
8868
assert(collateral_amount > 0, "INSUFFICIENT_COLLATERAL");
8969

90-
let sender = context.msg_sender().unwrap();
91-
let contract_address = context.this_address();
92-
93-
// Consume collateral notes
94-
let options = NoteGetterOptions::new();
95-
let notes: BoundedVec<UintNote, 16> = storage.collateral_balances.at(sender).pop_notes(options);
96-
97-
let mut total: u128 = 0;
98-
for i in 0..16 {
99-
if i < notes.len() {
100-
total += notes.get_unchecked(i).get_value();
101-
}
102-
}
103-
104-
assert(total >= collateral_amount, "INSUFFICIENT_COLLATERAL_BALANCE");
70+
let sender = self.context.msg_sender().unwrap();
71+
let contract_address = self.context.this_address();
10572

106-
// Return change
107-
let change = total - collateral_amount;
108-
if change > 0 {
109-
let change_note = UintNote::new(change, sender);
110-
storage.collateral_balances.at(sender).insert(change_note).emit(sender, MessageDelivery.UNCONSTRAINED_ONCHAIN);
111-
}
73+
// Consume collateral - sub handles note consumption and change
74+
self.storage.collateral_balances.at(sender).sub(collateral_amount)
75+
.deliver(MessageDelivery.UNCONSTRAINED_ONCHAIN);
11276

11377
// Create partial note for shares
114-
// Storage slots are required for deriving partial note locations.
115-
// This is because the partial note needs to be manually placed at that slot because we cannot use PrivateSet::insert().
116-
// We cannot use PrivateSet::insert() because the note does not exist yet, as it is not fully created in private.
117-
118-
let base_slot = if is_yes { PredictionMarket::storage_layout().yes_balances.slot } else { PredictionMarket::storage_layout().no_balances.slot };
119-
let storage_slot: Field = derive_storage_slot_in_map(base_slot, sender);
120-
let partial_note = UintNote::partial(sender, storage_slot, &mut context, sender, contract_address);
78+
// Use get_storage_slot() from the Owned wrapper - it handles owner derivation internally
79+
let storage_slot = if is_yes {
80+
self.storage.yes_balances.get_storage_slot()
81+
} else {
82+
self.storage.no_balances.get_storage_slot()
83+
};
84+
let partial_note = UintNote::partial(sender, storage_slot, self.context, sender, contract_address);
12185

12286
// Enqueue public call - sender address is NOT passed, preserving identity privacy
123-
PredictionMarket::at(contract_address)
124-
._process_buy(is_yes, collateral_amount, min_shares_out, partial_note)
125-
.enqueue(&mut context);
87+
self.enqueue_self._process_buy(is_yes, collateral_amount, min_shares_out, partial_note);
12688
}
12789

12890
/// Process buy in public context. Does not know who is buying.
129-
#[internal]
91+
#[only_self]
13092
#[external("public")]
13193
fn _process_buy(is_yes: bool, collateral_amount: u128, min_shares_out: u128, partial_note: PartialUintNote) {
132-
let yes_supply = storage.yes_supply.read();
133-
let no_supply = storage.no_supply.read();
134-
let liquidity = storage.total_liquidity.read();
94+
let yes_supply = self.storage.yes_supply.read();
95+
let no_supply = self.storage.no_supply.read();
96+
let liquidity = self.storage.total_liquidity.read();
13597

13698
let (shares_out, new_supply) = calculate_shares_out(
13799
collateral_amount,
@@ -142,35 +104,35 @@ pub contract PredictionMarket {
142104
assert(shares_out >= min_shares_out, "SLIPPAGE_EXCEEDED");
143105

144106
if is_yes {
145-
storage.yes_supply.write(new_supply);
107+
self.storage.yes_supply.write(new_supply);
146108
} else {
147-
storage.no_supply.write(new_supply);
109+
self.storage.no_supply.write(new_supply);
148110
}
149-
storage.total_liquidity.write(liquidity + collateral_amount);
111+
self.storage.total_liquidity.write(liquidity + collateral_amount);
150112

151-
partial_note.complete(&mut context, context.this_address(), shares_out);
113+
partial_note.complete(self.context, self.context.this_address(), shares_out);
152114
}
153115

154116
/// Get current price for YES or NO (returns value with PRICE_PRECISION decimals).
155117
#[external("utility")]
156-
unconstrained fn get_price(is_yes: bool) -> u128 {
157-
let yes_supply = storage.yes_supply.read();
158-
let no_supply = storage.no_supply.read();
118+
unconstrained fn get_price(is_yes: bool) -> pub u128 {
119+
let yes_supply = self.storage.yes_supply.read();
120+
let no_supply = self.storage.no_supply.read();
159121
calculate_new_price(if is_yes { yes_supply } else { no_supply }, yes_supply + no_supply)
160122
}
161123

162124
/// Get market state: (yes_supply, no_supply, total_liquidity).
163125
#[external("utility")]
164-
unconstrained fn get_market_state() -> (u128, u128, u128) {
165-
(storage.yes_supply.read(), storage.no_supply.read(), storage.total_liquidity.read())
126+
unconstrained fn get_market_state() -> pub (u128, u128, u128) {
127+
(self.storage.yes_supply.read(), self.storage.no_supply.read(), self.storage.total_liquidity.read())
166128
}
167129

168130
/// Quote shares out for a given collateral amount.
169131
#[external("utility")]
170-
unconstrained fn quote_buy(is_yes: bool, collateral_amount: u128) -> u128 {
171-
let yes_supply = storage.yes_supply.read();
172-
let no_supply = storage.no_supply.read();
173-
let liquidity = storage.total_liquidity.read();
132+
unconstrained fn quote_buy(is_yes: bool, collateral_amount: u128) -> pub u128 {
133+
let yes_supply = self.storage.yes_supply.read();
134+
let no_supply = self.storage.no_supply.read();
135+
let liquidity = self.storage.total_liquidity.read();
174136
let (shares_out, _) = calculate_shares_out(
175137
collateral_amount,
176138
if is_yes { yes_supply } else { no_supply },
@@ -181,54 +143,38 @@ pub contract PredictionMarket {
181143

182144
/// Quote price impact for a purchase.
183145
/// Returns: (price_before, price_after, impact_percentage)
184-
/// All values use PRICE_PRECISION (1_000_000) as the decimal base.
185146
#[external("utility")]
186-
unconstrained fn quote_price_impact(is_yes: bool, collateral_amount: u128) -> (u128, u128, u128) {
187-
let yes_supply = storage.yes_supply.read();
188-
let no_supply = storage.no_supply.read();
189-
let liquidity = storage.total_liquidity.read();
147+
unconstrained fn quote_price_impact(is_yes: bool, collateral_amount: u128) -> pub (u128, u128, u128) {
148+
let yes_supply = self.storage.yes_supply.read();
149+
let no_supply = self.storage.no_supply.read();
150+
let liquidity = self.storage.total_liquidity.read();
190151
calculate_price_impact(
191152
collateral_amount,
192153
if is_yes { yes_supply } else { no_supply },
193154
liquidity,
194155
)
195156
}
196157

197-
/// Get user's private collateral balance.
158+
/// Get user's private collateral balance (unconstrained view).
198159
#[external("utility")]
199-
unconstrained fn get_collateral_balance(user: AztecAddress) -> u128 {
200-
let notes: BoundedVec<UintNote, MAX_NOTES_PER_PAGE> = storage.collateral_balances.at(user).view_notes(NoteViewerOptions::new());
201-
let mut sum: u128 = 0;
202-
for i in 0..notes.len() {
203-
sum += notes.get_unchecked(i).get_value();
204-
}
205-
sum
160+
unconstrained fn get_collateral_balance(user: AztecAddress) -> pub u128 {
161+
self.storage.collateral_balances.at(user).balance_of()
206162
}
207163

208-
/// Get user's private YES token balance.
164+
/// Get user's private YES token balance (unconstrained view).
209165
#[external("utility")]
210-
unconstrained fn get_yes_balance(user: AztecAddress) -> u128 {
211-
let notes: BoundedVec<UintNote, MAX_NOTES_PER_PAGE> = storage.yes_balances.at(user).view_notes(NoteViewerOptions::new());
212-
let mut sum: u128 = 0;
213-
for i in 0..notes.len() {
214-
sum += notes.get_unchecked(i).get_value();
215-
}
216-
sum
166+
unconstrained fn get_yes_balance(user: AztecAddress) -> pub u128 {
167+
self.storage.yes_balances.at(user).balance_of()
217168
}
218169

219-
/// Get user's private NO token balance.
170+
/// Get user's private NO token balance (unconstrained view).
220171
#[external("utility")]
221-
unconstrained fn get_no_balance(user: AztecAddress) -> u128 {
222-
let notes: BoundedVec<UintNote, MAX_NOTES_PER_PAGE> = storage.no_balances.at(user).view_notes(NoteViewerOptions::new());
223-
let mut sum: u128 = 0;
224-
for i in 0..notes.len() {
225-
sum += notes.get_unchecked(i).get_value();
226-
}
227-
sum
172+
unconstrained fn get_no_balance(user: AztecAddress) -> pub u128 {
173+
self.storage.no_balances.at(user).balance_of()
228174
}
229175

230176
#[external("utility")]
231-
unconstrained fn get_admin() -> AztecAddress {
232-
storage.admin.read()
177+
unconstrained fn get_admin() -> pub AztecAddress {
178+
self.storage.admin.read()
233179
}
234180
}

0 commit comments

Comments
 (0)