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
24 changes: 19 additions & 5 deletions circuits/withdraw/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fn main(
amount: pub Field,
relayer: pub Field,
fee: pub Field,
denomination: pub Field,
) {
// -- Step 1: Reconstruct commitment --
// commitment = Hash(nullifier, secret)
Expand All @@ -82,6 +83,17 @@ fn main(
// Relayer must be zero if fee is zero
validation::validate_relayer(relayer, fee);

// Verify amount matches denomination
// Denomination values: 10=10000000, 100=100000000, 1000=1000000000, 10000=10000000000
let expected_amount = match denomination {
10 => 100_000_000,
100 => 1_000_000_000,
1000 => 10_000_000_000,
10000 => 100_000_000_000,
_ => 0,
};
assert(amount == expected_amount, "amount must match denomination");

// Bind recipient to prevent front-running
// (In Groth16, all pub inputs are automatically bound to the proof)
let _ = recipient;
Expand Down Expand Up @@ -109,13 +121,14 @@ fn test_valid_withdrawal() {

let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let recipient: Field = 0xABCD;
let amount: Field = 100_0000000; // 100 XLM in stroops
let amount: Field = 1_000_000_000; // 100 XLM in stroops
let relayer: Field = 0;
let fee: Field = 0;
let denomination: Field = 100; // Hundred denomination

main(
nullifier, secret, 0, hash_path,
root, nullifier_hash, recipient, amount, relayer, fee
root, nullifier_hash, recipient, amount, relayer, fee, denomination
);
}

Expand All @@ -129,13 +142,14 @@ fn test_withdrawal_with_relayer_fee() {

let nullifier_hash = hash::compute_nullifier_hash(nullifier, root);
let recipient: Field = 0xDEAD;
let amount: Field = 100_0000000;
let amount: Field = 1_000_000_000;
let relayer: Field = 0xBEEF;
let fee: Field = 1_0000000; // 1 XLM fee
let fee: Field = 10_000_000; // 1 XLM fee
let denomination: Field = 100; // Hundred denomination

main(
nullifier, secret, 0, hash_path,
root, nullifier_hash, recipient, amount, relayer, fee
root, nullifier_hash, recipient, amount, relayer, fee, denomination
);
}

Expand Down
33 changes: 19 additions & 14 deletions contracts/privacy_pool/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,30 @@ impl PrivacyPool {
/// Initialize the privacy pool.
///
/// Must be called once before any deposits or withdrawals.
/// Sets the admin, token, denomination, and verifying key.
/// Sets the admin, token, and verifying key.
pub fn initialize(
env: Env,
admin: Address,
token: Address,
denomination: Denomination,
vk: VerifyingKey,
) -> Result<(), Error> {
initialize::execute(env, admin, token, denomination, vk)
initialize::execute(env, admin, token, vk)
}

// ──────────────────────────────────────────────────────────
// Core Operations
// ──────────────────────────────────────────────────────────

/// Deposit into the shielded pool.
/// Deposit into the shielded pool for a specific denomination.
///
/// Transfers denomination amount and inserts commitment into Merkle tree.
pub fn deposit(
env: Env,
from: Address,
denomination: Denomination,
commitment: BytesN<32>,
) -> Result<(u32, BytesN<32>), Error> {
deposit::execute(env, from, commitment)
deposit::execute(env, from, denomination, commitment)
}

/// Withdraw from the shielded pool using a ZK proof.
Expand All @@ -64,19 +64,19 @@ impl PrivacyPool {
// View Functions
// ──────────────────────────────────────────────────────────

/// Returns the current Merkle root (most recent).
pub fn get_root(env: Env) -> Result<BytesN<32>, Error> {
view::get_root(env)
/// Returns the current Merkle root for a denomination (most recent).
pub fn get_root_by_denomination(env: Env, denomination: Denomination) -> Result<BytesN<32>, Error> {
view::get_root_by_denomination(env, denomination)
}

/// Returns the total number of deposits.
pub fn deposit_count(env: Env) -> u32 {
view::deposit_count(env)
/// Returns the total number of deposits for a denomination.
pub fn deposit_count_by_denomination(env: Env, denomination: Denomination) -> u32 {
view::deposit_count_by_denomination(env, denomination)
}

/// Check if a root is in the historical root buffer.
pub fn is_known_root(env: Env, root: BytesN<32>) -> bool {
view::is_known_root(env, root)
/// Check if a root is in the historical root buffer for a denomination.
pub fn is_known_root_for_denomination(env: Env, root: BytesN<32>, denomination: Denomination) -> bool {
view::is_known_root_for_denomination(env, root, denomination)
}

/// Check if a nullifier has been spent.
Expand All @@ -89,6 +89,11 @@ impl PrivacyPool {
view::get_config(env)
}

/// Returns all supported denominations.
pub fn get_all_denominations(env: Env) -> Vec<Denomination> {
view::get_all_denominations(env)
}

// ──────────────────────────────────────────────────────────
// Admin Functions
// ──────────────────────────────────────────────────────────
Expand Down
47 changes: 39 additions & 8 deletions contracts/privacy_pool/src/core/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
// Deposit Logic
// ============================================================

use soroban_sdk::{token, Address, BytesN, Env};
use soroban_sdk::{token, Address, BytesN, Env, Vec};

use crate::crypto::merkle;
use crate::storage::config;
use crate::types::errors::Error;
use crate::types::events::emit_deposit;
use crate::types::state::{Denomination, DataKey};
use crate::utils::validation;

/// Execute a deposit into the shielded pool.
/// Execute a deposit into the shielded pool for a specific denomination.
///
/// # Arguments
/// - `from` : depositor's Stellar address (must authorize)
/// - `commitment` : 32-byte field element = Hash(nullifier, secret)
/// - `from` : depositor's Stellar address (must authorize)
/// - `denomination`: which fixed amount to deposit
/// - `commitment` : 32-byte field element = Hash(nullifier, secret)
///
/// # Returns
/// `(leaf_index, merkle_root)` - store leaf_index with your note
Expand All @@ -24,9 +26,11 @@ use crate::utils::validation;
/// - `Error::PoolPaused` if pool is paused
/// - `Error::ZeroCommitment` if commitment is all zeros
/// - `Error::TreeFull` if pool is full (1,048,576 deposits)
/// - `Error::WrongAmount` if transferred amount doesn't match denomination
pub fn execute(
env: Env,
from: Address,
denomination: Denomination,
commitment: BytesN<32>,
) -> Result<(u32, BytesN<32>), Error> {
// Require authorization from the depositor
Expand All @@ -40,19 +44,46 @@ pub fn execute(
validation::require_non_zero_commitment(&env, &commitment)?;

// Transfer denomination amount from depositor to contract vault
let amount = pool_config.denomination.amount();
let amount = denomination.amount();
let token_client = token::Client::new(&env, &pool_config.token);
token_client.transfer(
&from,
&env.current_contract_address(),
&amount,
);

// Insert commitment into Merkle tree
let (leaf_index, new_root) = merkle::insert(&env, commitment.clone())?;
// Add denomination to list if not already present
add_denomination(&env, &denomination);

// Insert commitment into Merkle tree for this denomination
let denom_value = denomination.to_u32();
let (leaf_index, new_root) = merkle::insert(&env, denom_value, commitment.clone())?;

// Emit deposit event (no depositor address for privacy)
emit_deposit(&env, commitment, leaf_index, new_root.clone());
emit_deposit(&env, commitment, leaf_index, new_root.clone(), denomination);

Ok((leaf_index, new_root))
}

/// Add a denomination to the list of supported denominations if not already present
fn add_denomination(env: &Env, denomination: &Denomination) {
let key = DataKey::Denominations;
let mut denominations: Vec<Denomination> = env.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| Vec::new(env));

// Check if denomination is already in the list
let mut found = false;
for i in 0..denominations.len() {
if denominations.get(i).unwrap() == *denomination {
found = true;
break;
}
}

if !found {
denominations.push_back(denomination.clone());
env.storage().persistent().set(&key, &denominations);
}
}
5 changes: 1 addition & 4 deletions contracts/privacy_pool/src/core/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ use soroban_sdk::{Address, Env};
use crate::crypto::merkle;
use crate::storage::config;
use crate::types::errors::Error;
use crate::types::state::{Denomination, PoolConfig, VerifyingKey};
use crate::types::state::{PoolConfig, VerifyingKey};

/// Initialize the privacy pool with configuration.
///
/// # Arguments
/// - `admin` : address that can pause/update the pool
/// - `token` : token contract (use Stellar native XLM or USDC SAC)
/// - `denomination`: fixed deposit/withdrawal amount
/// - `vk` : Groth16 verifying key for the withdrawal circuit
///
/// # Errors
Expand All @@ -23,7 +22,6 @@ pub fn execute(
env: Env,
admin: Address,
token: Address,
denomination: Denomination,
vk: VerifyingKey,
) -> Result<(), Error> {
// Check if already initialized
Expand All @@ -35,7 +33,6 @@ pub fn execute(
let pool_config = PoolConfig {
admin,
token,
denomination,
tree_depth: merkle::TREE_DEPTH,
root_history_size: merkle::ROOT_HISTORY_SIZE,
paused: false,
Expand Down
33 changes: 22 additions & 11 deletions contracts/privacy_pool/src/core/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@
// View Functions - Read-only queries
// ============================================================

use soroban_sdk::{BytesN, Env};
use soroban_sdk::{BytesN, Env, Vec};

use crate::crypto::merkle;
use crate::storage::{config, nullifier};
use crate::types::errors::Error;
use crate::types::state::PoolConfig;
use crate::types::state::{PoolConfig, Denomination, DataKey};

/// Returns the current Merkle root (most recent).
pub fn get_root(env: Env) -> Result<BytesN<32>, Error> {
merkle::current_root(&env).ok_or(Error::NotInitialized)
/// Returns the current Merkle root for a denomination (most recent).
pub fn get_root_by_denomination(env: Env, denomination: Denomination) -> Result<BytesN<32>, Error> {
let denom_value = denomination.to_u32();
merkle::current_root(&env, denom_value).ok_or(Error::NotInitialized)
}

/// Returns the total number of deposits (= next leaf index).
pub fn deposit_count(env: Env) -> u32 {
merkle::get_tree_state(&env).next_index
/// Returns the total number of deposits for a denomination (= next leaf index).
pub fn deposit_count_by_denomination(env: Env, denomination: Denomination) -> u32 {
let denom_value = denomination.to_u32();
merkle::get_tree_state(&env, denom_value).next_index
}

/// Check if a root is in the historical root buffer.
pub fn is_known_root(env: Env, root: BytesN<32>) -> bool {
merkle::is_known_root(&env, &root)
/// Check if a root is in the historical root buffer for a denomination.
pub fn is_known_root_for_denomination(env: Env, root: BytesN<32>, denomination: Denomination) -> bool {
let denom_value = denomination.to_u32();
merkle::is_known_root(&env, denom_value, &root)
}

/// Check if a nullifier has been spent.
Expand All @@ -33,3 +36,11 @@ pub fn is_spent(env: Env, nullifier_hash: BytesN<32>) -> bool {
pub fn get_config(env: Env) -> Result<PoolConfig, Error> {
config::load(&env)
}

/// Returns all supported denominations.
pub fn get_all_denominations(env: Env) -> Vec<Denomination> {
env.storage()
.persistent()
.get(&DataKey::Denominations)
.unwrap_or_else(|| Vec::new(&env))
}
30 changes: 24 additions & 6 deletions contracts/privacy_pool/src/core/withdraw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// Withdrawal Logic
// ============================================================

use soroban_sdk::{token, Address, Env};
use soroban_sdk::{token, Address, Env, BytesN};

use crate::crypto::verifier;
use crate::crypto::{verifier, merkle};
use crate::storage::{config, nullifier};
use crate::types::errors::Error;
use crate::types::events::emit_withdraw;
use crate::types::state::{Proof, PublicInputs};
use crate::types::state::{Proof, PublicInputs, Denomination, DataKey};
use crate::utils::{address_decoder, validation};

/// Execute a withdrawal from the shielded pool using a ZK proof.
Expand All @@ -27,6 +27,7 @@ use crate::utils::{address_decoder, validation};
/// - `Error::NullifierAlreadySpent` if nullifier was already used
/// - `Error::FeeExceedsAmount` if fee > denomination amount
/// - `Error::InvalidProof` if Groth16 verification fails
/// - `Error::WrongAmount` if amount doesn't match denomination
pub fn execute(
env: Env,
proof: Proof,
Expand All @@ -36,10 +37,15 @@ pub fn execute(
let pool_config = config::load(&env)?;
validation::require_not_paused(&pool_config)?;

let denomination_amount = pool_config.denomination.amount();
// Decode denomination from public inputs
let denomination = decode_denomination(&env, &pub_inputs.denomination)?;
let denomination_amount = denomination.amount();

// Step 1: Validate root is in history
validation::require_known_root(&env, &pub_inputs.root)?;
// Step 1: Validate root is in history for this denomination
let denom_value = denomination.to_u32();
if !merkle::is_known_root(&env, denom_value, &pub_inputs.root) {
return Err(Error::UnknownRoot);
}

// Step 2: Check nullifier not already spent
validation::require_nullifier_unspent(&env, &pub_inputs.nullifier_hash)?;
Expand Down Expand Up @@ -79,11 +85,23 @@ pub fn execute(
relayer_opt.clone(),
fee,
denomination_amount,
denomination,
);

Ok(true)
}

/// Decode denomination from 32-byte field element
fn decode_denomination(env: &Env, denom_bytes: &BytesN<32>) -> Result<Denomination, Error> {
// Convert to u32 by taking the last 4 bytes
let bytes = denom_bytes.to_array();
let mut value_bytes = [0u8; 4];
value_bytes.copy_from_slice(&bytes[28..32]);
let value = u32::from_be_bytes(value_bytes);

Denomination::from_u32(value).ok_or(Error::WrongAmount)
}

/// Transfer funds to recipient and optionally to relayer.
fn transfer_funds(
env: &Env,
Expand Down
Loading