Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 108 additions & 0 deletions .github/workflows/prediction-market-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Prediction Market Tests

on:
push:
branches:
- main
pull_request:
branches:
- main
paths:
- "prediction_market_contract/**"
- ".github/workflows/prediction-market-tests.yml"
workflow_dispatch:

jobs:
prediction-market-tests:
name: Prediction Market Tests
runs-on: ubuntu-latest
env:
AZTEC_ENV: sandbox
AZTEC_VERSION: 3.0.0-devnet.4

steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.36

- name: Set up Docker
uses: docker/setup-buildx-action@v3

- name: Install Aztec CLI
run: |
curl -s https://install.aztec.network > tmp.sh
NON_INTERACTIVE=1 bash tmp.sh
rm tmp.sh

- name: Update path
run: echo "$HOME/.aztec/bin" >> $GITHUB_PATH

- name: Set Aztec version and start sandbox
run: |
aztec-up ${{ env.AZTEC_VERSION }}
docker tag aztecprotocol/aztec:${{ env.AZTEC_VERSION }} aztecprotocol/aztec:latest
aztec start --sandbox &

- name: Wait for sandbox to be ready
run: |
echo "Waiting for sandbox to start..."
MAX_RETRIES=60
for i in $(seq 1 $MAX_RETRIES); do
if curl -s http://localhost:8080/status >/dev/null 2>&1; then
echo "Sandbox is ready!"
break
fi
if [ $i -eq $MAX_RETRIES ]; then
echo "Sandbox failed to start after $MAX_RETRIES attempts"
exit 1
fi
echo "Waiting... ($i/$MAX_RETRIES)"
sleep 2
done

- name: Install project dependencies
working-directory: prediction_market_contract
run: bun install

- name: Run Noir unit tests
working-directory: prediction_market_contract
run: aztec test
timeout-minutes: 10

- name: Compile contract and generate artifacts
working-directory: prediction_market_contract
run: |
aztec-nargo compile
aztec-postprocess-contract
aztec codegen target -o artifacts

- name: Run end-to-end tests
working-directory: prediction_market_contract
run: bun test
timeout-minutes: 15

- name: Upload test results if failed
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-logs
path: |
prediction_market_contract/tests/**/*.log
retention-days: 7

- name: Cleanup
if: always()
run: |
echo "Stopping Aztec sandbox..."
pkill -f "aztec" || true
docker stop $(docker ps -q) || true
docker rm $(docker ps -a -q) || true
6 changes: 6 additions & 0 deletions prediction_market_contract/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
target/
artifacts/
node_modules/
dist/
*.log
.DS_Store
9 changes: 9 additions & 0 deletions prediction_market_contract/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "prediction_market_contract"
authors = [""]
compiler_version = ">=0.25.0"
type = "contract"

[dependencies]
aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.4", directory = "noir-projects/aztec-nr/aztec" }
uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.4", directory = "noir-projects/aztec-nr/uint-note" }
164 changes: 164 additions & 0 deletions prediction_market_contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Prediction Market Contract

A prediction market implementation on Aztec using a **Constant Sum Market Maker (CSMM)** for binary outcomes with **full identity privacy**.

## Overview

This contract allows users to:
- **Vote on binary outcomes** (YES/NO) **anonymously** - no one knows WHO voted
Copy link
Contributor

Choose a reason for hiding this comment

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

Would consider this a vote or a bet? Are users actually using this contract to make decisions, or just to bet on them?

- **Hold fully private balances** - collateral and share holdings are all hidden
- **Experience dynamic pricing** that adjusts based on market sentiment
- **Get slippage protection** to prevent adverse price movements
- **Single-transaction voting** using partial notes pattern

## Privacy Model

### What's Private

| Data | Privacy | Notes |
|------|---------|-------|
| Voter identity | **PRIVATE** | Public function doesn't receive sender address |
| Collateral balances | **PRIVATE** | Stored as private notes (UintNote) |
| Share balances | **PRIVATE** | Stored as private notes (UintNote) |
| Your vote (YES/NO) | **PRIVATE** | Hidden via partial notes |

### What's Public

| Data | Visibility | Why |
|------|------------|-----|
| Price changes | PUBLIC | Necessary for AMM pricing |
| Trade amounts | PUBLIC | Affects price movement |
| That someone bought YES/NO | PUBLIC | Observable from price changes |

### Privacy Architecture

The contract uses **partial notes** for private voting (like the [AMM contract](https://github.com/AztecProtocol/aztec-packages/tree/master/noir-projects/noir-contracts/contracts/amm_contract)):

```
1. buy_outcome() [PRIVATE]
|
+-- Consumes private collateral notes
+-- Creates change note if needed
+-- Creates partial note commitment (hides owner)
+-- Enqueues public call WITHOUT sender address
v
2. _process_buy() [PUBLIC]
|
+-- Calculates shares based on CSMM pricing
+-- Updates YES/NO supplies (price changes)
+-- Completes partial note with share amount
v
Shares appear in user's private balance (single tx!)
```

Key privacy feature: The public `_process_buy()` function **does not receive the sender address**. It only knows WHAT was traded, not WHO traded.

## Usage

### Voting Flow (Single Transaction)

```typescript
// Deposit collateral privately
await market.methods.deposit(1000n).send({ from: myAddress }).wait()

// Buy outcome privately - single transaction!
await market.methods.buy_outcome(
true, // is_yes
500n, // collateral_amount
900n, // min_shares_out (slippage protection)
).send({ from: myAddress }).wait()

// Your shares are immediately in your private balance!
const myBalance = await market.methods.get_yes_balance(myAddress).simulate({ from: myAddress })
```

### Collateral Management (All Private)

```typescript
// Deposit collateral (private)
await market.methods.deposit(1000n).send({ from: myAddress }).wait()

// Withdraw collateral (private)
await market.methods.withdraw(500n).send({ from: myAddress }).wait()

// Check balance (view private notes)
const balance = await market.methods.get_collateral_balance(myAddress).simulate({ from: myAddress })
```

## Development

### Prerequisites

```bash
# Install Aztec tools
bash -i <(curl -s https://install.aztec.network)
aztec-up 3.0.0-devnet.4

# Install npm dependencies
npm install
Copy link
Contributor

Choose a reason for hiding this comment

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

wdyt about updating this to be yarn install instead, to keep it consistent with our other repos/examples/docs? we pretty much use yarn everywhere else

```

### Building

```bash
# Compile the contract
aztec-nargo compile

# Post-process and generate TypeScript bindings
aztec-postprocess-contract
aztec codegen target -o artifacts
```

### Testing

#### Noir Unit Tests

```bash
aztec test
```

Tests the CSMM pricing functions:
- Share calculations at various price points
- Price calculations and invariants
- Edge cases

#### End-to-End Tests

```bash
# Start Aztec sandbox (in separate terminal)
aztec start --sandbox

# Run tests
npm test
```

Tests the full private voting flow:
- Contract deployment
- Private deposit/withdraw
- Private buy_outcome (single tx with partial notes)
- Price movements
- Balance privacy

## Constant Sum Market Maker (CSMM)

### Core Invariant
```
price_YES + price_NO = 1
```

### Pricing Formula
```
price_YES = yes_supply / (yes_supply + no_supply)
shares_out = collateral_in / current_price
```

### Example

```
Initial: YES=5000, NO=5000, price_YES=50%

Alice buys 1000 collateral of YES:
- Shares received: 1000 / 0.50 = 2000 YES
- New YES supply: 7000
- New price_YES: 7000 / 12000 = 58.3%
```
Loading