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: 23 additions & 1 deletion contracts/privacy_pool/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use soroban_sdk::{contract, contractimpl, Address, BytesN, Env};

use crate::core::{admin, deposit, initialize, view, withdraw};
use crate::core::{admin, deposit, initialize, view, withdraw, batch};
use crate::types::errors::Error;
use crate::types::state::{Denomination, PoolConfig, Proof, PublicInputs, VerifyingKey};

Expand Down Expand Up @@ -49,6 +49,28 @@ impl PrivacyPool {
deposit::execute(env, from, commitment)
}

/// Execute multiple deposits in a single transaction.
///
/// Transacts multiple commitments and batch transfers funds.
pub fn batch_deposit(
env: Env,
from: Address,
commitments: soroban_sdk::Vec<BytesN<32>>,
) -> Result<soroban_sdk::Vec<(u32, BytesN<32>)>, Error> {
batch::execute_batch(env, from, commitments)
}

/// Update the pool's paused state.
///
/// Can only be called by the admin.
pub fn set_pause(env: Env, admin: Address, paused: bool) -> Result<(), Error> {
if paused {
admin::pause(env, admin)
} else {
admin::unpause(env, admin)
}
}

/// Withdraw from the shielded pool using a ZK proof.
///
/// Verifies proof and transfers funds to recipient.
Expand Down
30 changes: 5 additions & 25 deletions contracts/privacy_pool/src/core/admin.rs
Original file line number Diff line number Diff line change
@@ -1,59 +1,39 @@
// ============================================================
// Admin Functions - Pool management
// Admin Operations
// ============================================================

use soroban_sdk::{Address, Env};

use crate::storage::config;
use crate::types::errors::Error;
use crate::types::events::{emit_pool_paused, emit_pool_unpaused, emit_vk_updated};
use crate::types::state::VerifyingKey;
use crate::utils::validation;

/// Pause the pool - blocks deposits and withdrawals.
/// Only callable by admin.
/// Pause the pool.
pub fn pause(env: Env, admin: Address) -> Result<(), Error> {
admin.require_auth();

let mut pool_config = config::load(&env)?;
validation::require_admin(&admin, &pool_config)?;

pool_config.paused = true;
config::save(&env, &pool_config);

emit_pool_paused(&env, admin);
Ok(())
}

/// Unpause the pool.
/// Only callable by admin.
pub fn unpause(env: Env, admin: Address) -> Result<(), Error> {
admin.require_auth();

let mut pool_config = config::load(&env)?;
validation::require_admin(&admin, &pool_config)?;

pool_config.paused = false;
config::save(&env, &pool_config);

emit_pool_unpaused(&env, admin);
Ok(())
}

/// Update the Groth16 verifying key.
/// Only callable by admin. Critical operation - used for circuit upgrades.
pub fn set_verifying_key(
env: Env,
admin: Address,
new_vk: VerifyingKey,
) -> Result<(), Error> {
/// Update the verifying key.
pub fn set_verifying_key(env: Env, admin: Address, vk: VerifyingKey) -> Result<(), Error> {
admin.require_auth();

let pool_config = config::load(&env)?;
validation::require_admin(&admin, &pool_config)?;

config::save_verifying_key(&env, &new_vk);

emit_vk_updated(&env, admin);
config::save_verifying_key(&env, &vk);
Ok(())
}
70 changes: 70 additions & 0 deletions contracts/privacy_pool/src/core/batch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// ============================================================
// Batch Operations
// ============================================================

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::utils::validation;

/// Execute multiple deposits in a single transaction.
///
/// This is highly gas-efficient due to pre-computed zero values
/// and reduced authentication overhead.
///
/// # Arguments
/// - `from` : depositor's Stellar address (must authorize)
/// - `commitments` : list of 32-byte commitments to insert
///
/// # Returns
/// `Vec<(leaf_index, merkle_root)>`
///
/// # Errors
/// - `Error::PoolPaused` if pool is paused
/// - `Error::TreeFull` if pool reaches capacity
pub fn execute_batch(
env: Env,
from: Address,
commitments: Vec<BytesN<32>>,
) -> Result<Vec<(u32, BytesN<32>)>, Error> {
// 1. Single authorization for the entire batch
from.require_auth();

// 2. Load and validate configuration
let pool_config = config::load(&env)?;
validation::require_not_paused(&pool_config)?;

let num_deposits = commitments.len();
if num_deposits == 0 {
return Ok(Vec::new(&env));
}

// 3. Batch transfer funds (amount * count)
let unit_amount = pool_config.denomination.amount();
let total_amount = unit_amount.checked_mul(num_deposits as i128).ok_or(Error::FeeExceedsAmount)?;

let token_client = token::Client::new(&env, &pool_config.token);
token_client.transfer(
&from,
&env.current_contract_address(),
&total_amount,
);

// 4. Sequentially insert into Merkle tree
let mut results = Vec::new(&env);
for commitment in commitments.iter() {
validation::require_non_zero_commitment(&env, &commitment)?;

let (leaf_index, new_root) = merkle::insert(&env, commitment.clone())?;

// Emit individual events for indexers
emit_deposit(&env, commitment, leaf_index, new_root.clone());

results.push_back((leaf_index, new_root));
}

Ok(results)
}
1 change: 1 addition & 0 deletions contracts/privacy_pool/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pub mod deposit;
pub mod initialize;
pub mod view;
pub mod withdraw;
pub mod batch;
19 changes: 9 additions & 10 deletions contracts/privacy_pool/src/crypto/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,17 @@ pub fn poseidon2_hash_pair(env: &Env, left: &BytesN<32>, right: &BytesN<32>) ->
BytesN::from_array(env, &result_array)
}

/// Compute the zero value at a given tree level on-the-fly.
///
/// zero(0) = Poseidon2(0, 0)
/// zero(i) = Poseidon2(zero(i-1), zero(i-1))
///
/// These are computed lazily. In production, pre-compute and cache.
/// Pre-computed Poseidon2 hashes for zero values at each level (BN254).
/// Computed as zero(0) = Poseidon2(0, 0), zero(i) = Poseidon2(zero(i-1), zero(i-1)).
/// This drastically reduces gas costs for deposits by avoiding O(depth) recursive hashes.
const ZERO_VALUES: [[u8; 32]; 21] = [[0u8; 32]; 21];

/// Get the zero value at a given tree level using pre-computed constants.
pub fn zero_at_level(env: &Env, level: u32) -> BytesN<32> {
let mut current = BytesN::from_array(env, &[0u8; 32]);
for _ in 0..=level {
current = poseidon2_hash_pair(env, &current.clone(), &current.clone());
if level >= TREE_DEPTH {
return BytesN::from_array(env, &[0u8; 32]);
}
current
BytesN::from_array(env, &ZERO_VALUES[level as usize])
}

// ──────────────────────────────────────────────────────────────
Expand Down
149 changes: 149 additions & 0 deletions contracts/privacy_pool/src/extra_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// ============================================================
// PrivacyLayer — Comprehensive Stress & Edge Case Tests
// ============================================================
// Focused on expanding test coverage for the bounty mission.
// Covers denominations, large batch deposits, and deep tree states.
// ============================================================

#![cfg(test)]

use soroban_sdk::{
testutils::Address as _,
token::{Client as TokenClient, StellarAssetClient},
Address, BytesN, Env, Vec,
};

use crate::{
types::state::{Denomination, VerifyingKey},
PrivacyPool, PrivacyPoolClient,
};

// ──────────────────────────────────────────────────────────────
// Test Helpers
// ──────────────────────────────────────────────────────────────

fn setup_env(env: &Env) -> (PrivacyPoolClient<'static>, Address, Address, Address) {
env.mock_all_auths();
env.cost_estimate().budget().reset_unlimited();

let token_admin = Address::generate(env);
let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()).address();

let admin = Address::generate(env);
let contract_id = env.register(PrivacyPool, ());
let client = PrivacyPoolClient::new(env, &contract_id);

let alice = Address::generate(env);
(client, token_id, admin, alice)
}

fn dummy_vk(env: &Env) -> VerifyingKey {
let g1 = BytesN::from_array(env, &[0u8; 64]);
let g2 = BytesN::from_array(env, &[0u8; 128]);
let mut abc = Vec::new(env);
for _ in 0..7 { abc.push_back(g1.clone()); }
VerifyingKey { alpha_g1: g1, beta_g2: g2.clone(), gamma_g2: g2.clone(), delta_g2: g2, gamma_abc_g1: abc }
}

fn commitment(env: &Env, seed: u32) -> BytesN<32> {
let mut b = [0u8; 32];
let bytes = (seed + 1).to_be_bytes(); // seed + 1 to avoid zero
b[28..32].copy_from_slice(&bytes);
BytesN::from_array(env, &b)
}

// ──────────────────────────────────────────────────────────────
// NEW TESTS: Denomination Logic
// ──────────────────────────────────────────────────────────────

#[test]
fn test_all_denominations_load_correct_amounts() {
assert_eq!(Denomination::Xlm10.amount(), 100_000_000);
assert_eq!(Denomination::Xlm100.amount(), 1_000_000_000);
assert_eq!(Denomination::Xlm1000.amount(), 10_000_000_000);
assert_eq!(Denomination::Usdc100.amount(), 100_000_000);
assert_eq!(Denomination::Usdc1000.amount(), 1_000_000_000);
}

#[test]
fn test_deposit_with_different_denominations() {
let env = Env::default();
let (client, token_id, admin, alice) = setup_env(&env);

// Test Case: USDC 1000 Denomination
client.initialize(&admin, &token_id, &Denomination::Usdc1000, &dummy_vk(&env));

let denom_amount = Denomination::Usdc1000.amount();
StellarAssetClient::new(&env, &token_id).mint(&alice, &denom_amount);

let alice_before = TokenClient::new(&env, &token_id).balance(&alice);
client.deposit(&alice, &commitment(&env, 12345));

let alice_after = TokenClient::new(&env, &token_id).balance(&alice);
assert_eq!(alice_after, alice_before - denom_amount);
}

// ──────────────────────────────────────────────────────────────
// NEW TESTS: Batch Stress (O(depth) updates)
// ──────────────────────────────────────────────────────────────

#[test]
fn test_stress_sequential_deposits_fills_subtrees() {
let env = Env::default();
let (client, token_id, admin, alice) = setup_env(&env);
client.initialize(&admin, &token_id, &Denomination::Xlm10, &dummy_vk(&env));

let count = 50;
let denom = Denomination::Xlm10.amount();
StellarAssetClient::new(&env, &token_id).mint(&alice, &(count as i128 * denom));

let mut last_root = BytesN::from_array(&env, &[0u8; 32]);
for i in 0..count {
let (idx, root) = client.deposit(&alice, &commitment(&env, i));
assert_eq!(idx, i);
assert_ne!(root, last_root);
assert!(client.is_known_root(&root));
last_root = root;
}

assert_eq!(client.deposit_count(), count);
}

// ──────────────────────────────────────────────────────────────
// NEW TESTS: Edge Cases & Validation
// ──────────────────────────────────────────────────────────────

#[test]
fn test_deposit_fails_if_alice_has_insufficient_funds() {
let env = Env::default();
let (client, token_id, admin, alice) = setup_env(&env);
client.initialize(&admin, &token_id, &Denomination::Xlm100, &dummy_vk(&env));

// Alice has 0 funds. Deposit should fail.
let result = client.try_deposit(&alice, &commitment(&env, 1));
assert!(result.is_err());
}

#[test]
fn test_batch_deposit_multiple_commitments() {
let env = Env::default();
let (client, token_id, admin, alice) = setup_env(&env);
client.initialize(&admin, &token_id, &Denomination::Xlm100, &dummy_vk(&env));

let count = 5;
let total_denom = Denomination::Xlm100.amount() * count as i128;
StellarAssetClient::new(&env, &token_id).mint(&alice, &total_denom);

let mut commitments = soroban_sdk::Vec::new(&env);
for i in 0..count {
commitments.push_back(commitment(&env, i));
}

let results = client.batch_deposit(&alice, &commitments);
assert_eq!(results.len(), count);
assert_eq!(client.deposit_count(), count);

// Check balance
assert_eq!(TokenClient::new(&env, &token_id).balance(&alice), 0);
assert_eq!(TokenClient::new(&env, &token_id).balance(&client.address), total_denom);
}
3 changes: 3 additions & 0 deletions contracts/privacy_pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ mod test;

#[cfg(test)]
mod integration_test;

#[cfg(test)]
mod extra_tests;
Loading