pool/pool.rs
1//! Privacy Pool Contract
2//!
3//! This contract implements a privacy-preserving transaction pool with embedded
4//! policy (membership and non-membership in an association set).
5//! It enables users to deposit, transfer, and withdraw
6//! tokens while maintaining transaction privacy through zero-knowledge proofs.
7//!
8//! # Architecture
9//!
10//! The contract maintains:
11//! - A Merkle tree of commitments (via `MerkleTreeWithHistory`)
12//! - A nullifier set to track spent UTXOs
13//! - Token integration for deposits and withdrawals
14
15#![allow(clippy::too_many_arguments)]
16use crate::merkle_with_history::{Error as MerkleError, MerkleTreeWithHistory};
17use contract_types::{Groth16Error, Groth16Proof};
18use soroban_sdk::{
19 Address, Bytes, BytesN, Env, I256, Map, U256, Vec, contract, contractclient, contracterror,
20 contractevent, contractimpl, contracttype, crypto::bn254::Fr, token::TokenClient, xdr::ToXdr,
21};
22use soroban_utils::constants::bn256_modulus;
23
24/// Contract error types for the privacy pool
25#[contracterror]
26#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
27#[repr(u32)]
28pub enum Error {
29 /// Caller is not authorized to perform this operation
30 NotAuthorized = 1,
31 /// Merkle tree has reached maximum capacity
32 MerkleTreeFull = 2,
33 /// Contract has already been initialized
34 AlreadyInitialized = 3,
35 /// Invalid Merkle tree levels configuration
36 WrongLevels = 4,
37 /// Internal error: next leaf index is not even
38 NextIndexNotEven = 5,
39 /// External amount is invalid (negative or exceeds 2^248)
40 WrongExtAmount = 6,
41 /// Zero-knowledge proof verification failed or proof is empty
42 InvalidProof = 7,
43 /// Provided Merkle root is not in the recent history
44 UnknownRoot = 8,
45 /// Nullifier has already been spent (double-spend attempt)
46 AlreadySpentNullifier = 9,
47 /// External data hash does not match the provided data
48 WrongExtHash = 10,
49 /// Contract is not initialized
50 NotInitialized = 11,
51}
52
53/// Conversion from MerkleTreeWithHistory errors to pool contract errors
54/// Errors from MerkleTreeWithHistory are not `contracterror`
55impl From<MerkleError> for Error {
56 fn from(e: MerkleError) -> Self {
57 match e {
58 MerkleError::AlreadyInitialized => Error::AlreadyInitialized,
59 MerkleError::MerkleTreeFull => Error::MerkleTreeFull,
60 MerkleError::WrongLevels => Error::WrongLevels,
61 MerkleError::NextIndexNotEven => Error::NextIndexNotEven,
62 MerkleError::NotInitialized => Error::NotInitialized,
63 }
64 }
65}
66
67/// Zero-knowledge proof data for a transaction
68///
69/// Contains all the cryptographic data needed to verify a transaction,
70/// including the proof itself, public inputs, and nullifiers.
71#[contracttype]
72pub struct Proof {
73 /// The serialized zero-knowledge proof
74 pub proof: Groth16Proof,
75 /// Merkle root the proof was generated against
76 pub root: U256,
77 /// Nullifiers for spent input UTXOs (prevents double-spending)
78 pub input_nullifiers: Vec<U256>,
79 /// Commitment for the first output UTXO
80 pub output_commitment0: U256,
81 /// Commitment for the second output UTXO
82 pub output_commitment1: U256,
83 /// Net public amount (deposit - withdrawal, modulo field size)
84 pub public_amount: U256,
85 /// Hash of the external data (binds proof to transaction parameters)
86 pub ext_data_hash: BytesN<32>,
87 /// Merkle root the policy membership proof was generated against
88 pub asp_membership_root: U256,
89 /// Merkle root the policy NON-membership proof was generated against
90 pub asp_non_membership_root: U256,
91}
92
93/// External data for a transaction
94///
95/// Contains public information about the transaction that is hashed and
96/// included in the zero-knowledge proof to bind the proof to specific
97/// transaction parameters (e.g. recipient address).
98#[contracttype]
99#[derive(Clone)]
100pub struct ExtData {
101 /// Recipient address for withdrawals
102 pub recipient: Address,
103 /// External amount: positive for deposits, negative for withdrawals
104 pub ext_amount: I256,
105 /// Encrypted data for the first output UTXO
106 pub encrypted_output0: Bytes,
107 /// Encrypted data for the second output UTXO
108 pub encrypted_output1: Bytes,
109}
110
111/// Hash external data using Keccak256
112///
113/// Serializes the external data to XDR, hashes it with Keccak256,
114/// and reduces the result modulo the BN256 field size.
115///
116/// # Arguments
117///
118/// * `env` - The Soroban environment
119/// * `ext` - The external data to hash
120///
121/// # Returns
122///
123/// Returns the 32-byte hash of the external data
124pub fn hash_ext_data(env: &Env, ext: &ExtData) -> BytesN<32> {
125 let payload = ext.clone().to_xdr(env);
126 let digest: BytesN<32> = env.crypto().keccak256(&payload).into();
127 let digest_u256 = U256::from_be_bytes(env, &Bytes::from(digest));
128 let reduced = digest_u256.rem_euclid(&bn256_modulus(env));
129 let mut buf = [0u8; 32];
130 reduced.to_be_bytes().copy_into_slice(&mut buf);
131 BytesN::from_array(env, &buf)
132}
133
134/// User account registration data
135///
136/// Used for registering a user's public key to enable encrypted communication
137/// for receiving transfers.
138/// Not required to interact with the pool. But facilitates in-pool transfers
139/// via events. As parties can learn about each other public key.
140#[contracttype]
141pub struct Account {
142 /// Owner address of the account
143 pub owner: Address,
144 /// X25519 encryption public key for encrypting note data (32 bytes)
145 pub encryption_key: Bytes,
146 /// BN254 note public key for creating commitments (32 bytes)
147 pub note_key: Bytes,
148}
149
150// Contract clients for cross-contract dependencies
151#[contractclient(crate_path = "soroban_sdk", name = "ASPMembershipClient")]
152pub trait ASPMembershipInterface {
153 fn get_root(env: Env) -> Result<U256, soroban_sdk::Error>;
154}
155
156#[contractclient(crate_path = "soroban_sdk", name = "ASPNonMembershipClient")]
157pub trait ASPNonMembershipInterface {
158 fn get_root(env: Env) -> Result<U256, soroban_sdk::Error>;
159}
160
161#[contractclient(crate_path = "soroban_sdk", name = "CircomGroth16VerifierClient")]
162pub trait CircomGroth16VerifierInterface {
163 fn verify(env: Env, proof: Groth16Proof, public_inputs: Vec<Fr>) -> Result<bool, Groth16Error>;
164}
165
166/// Storage keys for contract persistent data
167#[contracttype]
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub(crate) enum DataKey {
170 /// Administrator address with permissions to modify contract settings
171 Admin,
172 /// Address of the token contract used for deposits/withdrawals
173 Token,
174 /// Address of the ZK proof verifier contract
175 Verifier,
176 /// Maximum allowed deposit amount per transaction
177 MaximumDepositAmount,
178 /// Map of spent nullifiers (nullifier -> bool)
179 Nullifiers,
180 /// Address of the ASP Membership contract
181 ASPMembership,
182 /// Address of the ASP Non-Membership contract
183 ASPNonMembership,
184}
185
186/// Event emitted when a new commitment is added to the Merkle tree
187///
188/// This event allows off-chain observers to track new UTXOs and decrypt
189/// outputs intended for them.
190#[contractevent]
191#[derive(Clone)]
192pub struct NewCommitmentEvent {
193 /// The commitment hash added to the tree
194 #[topic]
195 pub commitment: U256,
196 /// Index position in the Merkle tree
197 pub index: u32,
198 /// Encrypted output data (decryptable by the recipient)
199 pub encrypted_output: Bytes,
200}
201
202/// Event emitted when a nullifier is spent
203///
204/// This event allows off-chain observers to track which UTXOs have been spent.
205#[contractevent]
206#[derive(Clone)]
207pub struct NewNullifierEvent {
208 /// The nullifier that was spent
209 #[topic]
210 pub nullifier: U256,
211}
212
213/// Event emitted when a user registers their public keys
214///
215/// This event allows other users to discover keys for sending private
216/// transfers. Two key types are required:
217/// - encryption_key: X25519 key for encrypting note data (amount, blinding)
218/// - note_key: BN254 key for creating commitments in the ZK circuit
219#[contractevent]
220#[derive(Clone)]
221pub struct PublicKeyEvent {
222 /// Address of the account owner
223 #[topic]
224 pub owner: Address,
225 /// X25519 encryption public key
226 pub encryption_key: Bytes,
227 /// BN254 note public key
228 pub note_key: Bytes,
229}
230
231/// Privacy Pool Contract
232///
233/// Implements a private transaction pool.
234/// Users can deposit tokens, perform private transfers, and withdraw while
235/// maintaining transaction privacy through zero-knowledge proofs.
236#[contract]
237pub struct PoolContract;
238
239#[contractimpl]
240impl PoolContract {
241 /// Constructor: initialize the privacy pool contract
242 ///
243 /// Sets up the contract with the specified token, verifier, and Merkle tree
244 /// configuration. This function can only be called once.
245 ///
246 /// # Arguments
247 ///
248 /// * `env` - The Soroban environment
249 /// * `admin` - Address of the contract administrator
250 /// * `token` - Address of the token contract for deposits/withdrawals
251 /// * `verifier` - Address of the ZK proof verifier contract
252 /// * `asp_membership` - Address of the ASP Membership contract
253 /// * `asp_non_membership` - Address of the ASP Non-Membership contract
254 /// * `maximum_deposit_amount` - Maximum allowed deposit per transaction
255 /// * `levels` - Number of levels in the commitment Merkle tree (1-32)
256 ///
257 /// # Returns
258 ///
259 /// Returns `Ok(())` on success, or an error if already initialized or
260 /// invalid configuration
261 pub fn __constructor(
262 env: Env,
263 admin: Address,
264 token: Address,
265 verifier: Address,
266 asp_membership: Address,
267 asp_non_membership: Address,
268 maximum_deposit_amount: U256,
269 levels: u32,
270 ) -> Result<(), Error> {
271 env.storage().persistent().set(&DataKey::Admin, &admin);
272 env.storage().persistent().set(&DataKey::Token, &token);
273 env.storage()
274 .persistent()
275 .set(&DataKey::Verifier, &verifier);
276 env.storage()
277 .persistent()
278 .set(&DataKey::ASPMembership, &asp_membership);
279 env.storage()
280 .persistent()
281 .set(&DataKey::ASPNonMembership, &asp_non_membership);
282 env.storage()
283 .persistent()
284 .set(&DataKey::MaximumDepositAmount, &maximum_deposit_amount);
285 env.storage()
286 .persistent()
287 .set(&DataKey::Nullifiers, &Map::<U256, bool>::new(&env));
288
289 // Initialize the Merkle tree for commitment storage
290 MerkleTreeWithHistory::init(&env, levels)?;
291
292 Ok(())
293 }
294
295 /// Maximum absolute external amount allowed (2^248)
296 ///
297 /// This limit ensures amounts fit within field arithmetic constraints.
298 fn max_ext_amount(env: &Env) -> U256 {
299 U256::from_parts(env, 0x0100_0000_0000_0000, 0, 0, 0)
300 }
301
302 /// Convert a non-negative I256 to i128 with bounds checking
303 ///
304 /// # Arguments
305 ///
306 /// * `env` - The Soroban environment
307 /// * `v` - The I256 value to convert
308 ///
309 /// # Returns
310 ///
311 /// Returns `Ok(i128)` if the value is non-negative and fits in i128,
312 /// or `Err(Error::WrongExtAmount)` otherwise
313 fn i256_to_i128_nonneg(env: &Env, v: &I256) -> Result<i128, Error> {
314 if *v < I256::from_i32(env, 0) {
315 return Err(Error::WrongExtAmount);
316 }
317 v.to_i128().ok_or(Error::WrongExtAmount)
318 }
319
320 /// Calculate the public amount from external amount
321 ///
322 /// Computes `public_amount = ext_amount` in the BN256 field.
323 /// For positive results, returns the value directly.
324 /// For negative results, returns `FIELD_SIZE - |public_amount|`.
325 ///
326 /// # Arguments
327 ///
328 /// * `env` - The Soroban environment
329 /// * `ext_amount` - External amount (positive for deposit, negative for
330 /// withdrawal)
331 ///
332 /// # Returns
333 ///
334 /// Returns the public amount as U256 in the BN256 field, or an error
335 /// if the amounts exceed limits
336 fn calculate_public_amount(env: &Env, ext_amount: I256) -> Result<U256, Error> {
337 let abs_ext = Self::i256_abs_to_u256(env, &ext_amount);
338 if abs_ext >= Self::max_ext_amount(env) {
339 return Err(Error::WrongExtAmount);
340 }
341
342 let zero = I256::from_i32(env, 0);
343
344 if ext_amount >= zero {
345 let pa_bytes = ext_amount.to_be_bytes();
346 Ok(U256::from_be_bytes(env, &pa_bytes))
347 } else {
348 // Negative: compute FIELD_SIZE - |ext_amount|
349 let neg = zero.sub(&ext_amount);
350 let neg_bytes = neg.to_be_bytes();
351 let neg_u256 = U256::from_be_bytes(env, &neg_bytes);
352
353 let field = bn256_modulus(env);
354 Ok(field.sub(&neg_u256))
355 }
356 }
357
358 /// Check if a nullifier has already been spent
359 ///
360 /// # Arguments
361 ///
362 /// * `env` - The Soroban environment
363 /// * `n` - The nullifier to check
364 ///
365 /// # Returns
366 ///
367 /// Returns `true` if the nullifier has been spent, `false` otherwise
368 fn is_spent(env: &Env, n: &U256) -> Result<bool, Error> {
369 let nulls = Self::get_nullifiers(env)?;
370 Ok(nulls.get(n.clone()).unwrap_or(false))
371 }
372
373 /// Mark a nullifier as spent
374 ///
375 /// # Arguments
376 ///
377 /// * `env` - The Soroban environment
378 /// * `n` - The nullifier to mark as spent
379 fn mark_spent(env: &Env, n: &U256) -> Result<(), Error> {
380 let mut nulls = Self::get_nullifiers(env)?;
381 nulls.set(n.clone(), true);
382 Self::set_nullifiers(env, &nulls);
383 Ok(())
384 }
385
386 /// Verify a zero-knowledge proof
387 ///
388 /// # Arguments
389 ///
390 /// * `env` - The Soroban environment
391 /// * `proof` - The proof to verify
392 ///
393 /// # Returns
394 ///
395 /// Returns `true` if the proof is valid, `false` otherwise
396 fn verify_proof(env: &Env, proof: &Proof) -> Result<bool, Error> {
397 // Check proof is not empty
398 if proof.proof.is_empty() {
399 return Err(Error::InvalidProof);
400 }
401 let verifier = Self::get_verifier(env)?;
402 let client = CircomGroth16VerifierClient::new(env, &verifier);
403
404 // Public inputs expected by the Circom Transaction circuit:
405 // Order is important. Order is defined by the order in which the signals were
406 // declared in the circuit. The current order is [root, public_amount,
407 // ext_data_hash, asp_membership_root, asp_non_membership_root, input
408 // nullifiers, output_commitment0, output_commitment1]
409 let mut public_inputs: Vec<Fr> = Vec::new(env);
410 public_inputs.push_back(Fr::from_bytes(Self::u256_to_bytes(env, &proof.root)));
411 public_inputs.push_back(Fr::from_bytes(Self::u256_to_bytes(
412 env,
413 &proof.public_amount,
414 )));
415 public_inputs.push_back(Fr::from_bytes(proof.ext_data_hash.clone()));
416 // Add policy roots. Order is important.
417 for _ in 0..proof.input_nullifiers.len() {
418 public_inputs.push_back(Fr::from_bytes(Self::u256_to_bytes(
419 env,
420 &proof.asp_membership_root,
421 )));
422 }
423 for _ in 0..proof.input_nullifiers.len() {
424 public_inputs.push_back(Fr::from_bytes(Self::u256_to_bytes(
425 env,
426 &proof.asp_non_membership_root,
427 )));
428 }
429 for nullifier in proof.input_nullifiers.iter() {
430 public_inputs.push_back(Fr::from_bytes(Self::u256_to_bytes(env, &nullifier)));
431 }
432 public_inputs.push_back(Fr::from_bytes(Self::u256_to_bytes(
433 env,
434 &proof.output_commitment0,
435 )));
436 public_inputs.push_back(Fr::from_bytes(Self::u256_to_bytes(
437 env,
438 &proof.output_commitment1,
439 )));
440
441 let is_valid = client.verify(&proof.proof, &public_inputs);
442
443 Ok(is_valid)
444 }
445
446 /// Hash external data using Keccak256
447 ///
448 /// Serializes the external data to XDR, hashes it with Keccak256,
449 /// and reduces the result modulo the BN256 field size.
450 ///
451 /// # Arguments
452 ///
453 /// * `env` - The Soroban environment
454 /// * `ext` - The external data to hash
455 ///
456 /// # Returns
457 ///
458 /// Returns the 32-byte hash of the external data
459 fn hash_ext_data(env: &Env, ext: &ExtData) -> BytesN<32> {
460 hash_ext_data(env, ext)
461 }
462
463 /// Convert I256 to its absolute value as U256
464 ///
465 /// # Arguments
466 ///
467 /// * `env` - The Soroban environment
468 /// * `v` - The I256 value
469 ///
470 /// # Returns
471 ///
472 /// Returns the absolute value of `v` as U256
473 fn i256_abs_to_u256(env: &Env, v: &I256) -> U256 {
474 let zero = I256::from_i32(env, 0);
475 let abs = if *v >= zero { v.clone() } else { zero.sub(v) };
476 U256::from_be_bytes(env, &abs.to_be_bytes())
477 }
478
479 /// Execute a shielded transaction with deposit handling
480 ///
481 /// This is the main entry point for users to interact with the pool.
482 /// If `ext_amount > 0`, tokens are transferred from the sender to the pool
483 /// before processing the transaction.
484 ///
485 /// # Arguments
486 ///
487 /// * `env` - The Soroban environment
488 /// * `proof` - Zero-knowledge proof and public inputs
489 /// * `ext_data` - External transaction data
490 /// * `sender` - Address of the transaction sender (must authorize funding
491 /// transaction)
492 ///
493 /// # Returns
494 ///
495 /// Returns `Ok(())` on success, or an error if validation fails
496 pub fn transact(
497 env: &Env,
498 proof: Proof,
499 ext_data: ExtData,
500 sender: Address,
501 ) -> Result<(), Error> {
502 sender.require_auth();
503 let token = Self::get_token(env)?;
504 let token_client = TokenClient::new(env, &token);
505 let zero = I256::from_i32(env, 0);
506
507 // Handle deposit if ext_amount > 0
508 if ext_data.ext_amount > zero {
509 let deposit_u = U256::from_be_bytes(env, &ext_data.ext_amount.to_be_bytes());
510 let max = Self::get_maximum_deposit(env)?;
511 if deposit_u > max {
512 return Err(Error::WrongExtAmount);
513 }
514 let this = env.current_contract_address();
515 let amount = Self::i256_to_i128_nonneg(env, &ext_data.ext_amount)?;
516 token_client.transfer(&sender, &this, &amount);
517 }
518
519 Self::internal_transact(env, proof, ext_data)
520 }
521
522 /// Process a private transaction
523 ///
524 /// Validates the proof and all public inputs, marks nullifiers as spent,
525 /// processes withdrawals, and inserts new commitments into the Merkle tree.
526 ///
527 /// # Arguments
528 ///
529 /// * `env` - The Soroban environment
530 /// * `proof` - Zero-knowledge proof and public inputs
531 /// * `ext_data` - External transaction data
532 ///
533 /// # Returns
534 ///
535 /// Returns `Ok(())` on success, or an error if any validation fails
536 ///
537 /// # Validation Steps
538 ///
539 /// 1. Verify Merkle root is in recent history
540 /// 2. Verify no nullifiers have been spent
541 /// 3. Verify external data hash matches
542 /// 4. Verify public amount calculation
543 /// 5. Verify zero-knowledge proof
544 fn internal_transact(env: &Env, proof: Proof, ext_data: ExtData) -> Result<(), Error> {
545 // 1. Merkle root check
546 if !MerkleTreeWithHistory::is_known_root(env, &proof.root)? {
547 return Err(Error::UnknownRoot);
548 }
549 // 2. Nullifier checks (prevent double-spending)
550 for n in proof.input_nullifiers.iter() {
551 if Self::is_spent(env, &n)? {
552 return Err(Error::AlreadySpentNullifier);
553 }
554 }
555 // 3. External data hash check
556 let ext_hash = Self::hash_ext_data(env, &ext_data);
557 if ext_hash != proof.ext_data_hash {
558 return Err(Error::WrongExtHash);
559 }
560
561 // 4. Public amount check
562 let expected_public_amount =
563 Self::calculate_public_amount(env, ext_data.ext_amount.clone())?;
564 if proof.public_amount != expected_public_amount {
565 return Err(Error::WrongExtAmount);
566 }
567
568 // ASP root validation
569 let member_root = Self::get_asp_membership_root(env)?;
570 let non_member_root = Self::get_asp_non_membership_root(env)?;
571 if member_root != proof.asp_membership_root
572 || non_member_root != proof.asp_non_membership_root
573 {
574 return Err(Error::InvalidProof);
575 }
576
577 // 5. ZK proof verification
578 if !Self::verify_proof(env, &proof)? {
579 return Err(Error::InvalidProof);
580 }
581
582 // 6. Mark nullifiers as spent
583 for n in proof.input_nullifiers.iter() {
584 let _ = Self::mark_spent(env, &n);
585 NewNullifierEvent { nullifier: n }.publish(env);
586 }
587
588 // 7. Process withdrawal if ext_amount < 0
589 let token = Self::get_token(env)?;
590 let token_client = TokenClient::new(env, &token);
591 let this = env.current_contract_address();
592 let zero = I256::from_i32(env, 0);
593
594 if ext_data.ext_amount < zero {
595 let abs = zero.sub(&ext_data.ext_amount);
596 let amount: i128 = Self::i256_to_i128_nonneg(env, &abs)?;
597 token_client.transfer(&this, &ext_data.recipient, &amount);
598 }
599
600 // 9. Insert new commitments into Merkle tree
601 let (idx_0, idx_1) = MerkleTreeWithHistory::insert_two_leaves(
602 env,
603 proof.output_commitment0.clone(),
604 proof.output_commitment1.clone(),
605 )?;
606
607 // 10. Emit commitment events
608 NewCommitmentEvent {
609 commitment: proof.output_commitment0,
610 index: idx_0,
611 encrypted_output: ext_data.encrypted_output0.clone(),
612 }
613 .publish(env);
614
615 NewCommitmentEvent {
616 commitment: proof.output_commitment1,
617 index: idx_1,
618 encrypted_output: ext_data.encrypted_output1.clone(),
619 }
620 .publish(env);
621
622 Ok(())
623 }
624
625 /// Register a user's public encryption key
626 ///
627 /// Allows users to publish their public key so others can send them
628 /// encrypted outputs for private transfers.
629 /// The account owner must authorize this call
630 ///
631 /// # Arguments
632 ///
633 /// * `env` - The Soroban environment
634 /// * `account` - Account data containing owner address and public key
635 pub fn register(env: Env, account: Account) {
636 account.owner.require_auth();
637 PublicKeyEvent {
638 owner: account.owner,
639 encryption_key: account.encryption_key,
640 note_key: account.note_key,
641 }
642 .publish(&env);
643 }
644
645 // ========== Storage Getters and Setters ==========
646
647 /// Get the nullifiers map from storage
648 fn get_nullifiers(env: &Env) -> Result<Map<U256, bool>, Error> {
649 env.storage()
650 .persistent()
651 .get(&DataKey::Nullifiers)
652 .ok_or(Error::NotInitialized)
653 }
654
655 /// Save the nullifiers map to storage
656 fn set_nullifiers(env: &Env, m: &Map<U256, bool>) {
657 env.storage().persistent().set(&DataKey::Nullifiers, m);
658 }
659
660 /// Get the token contract address
661 fn get_token(env: &Env) -> Result<Address, Error> {
662 env.storage()
663 .persistent()
664 .get(&DataKey::Token)
665 .ok_or(Error::NotInitialized)
666 }
667
668 /// Get the maximum deposit amount
669 fn get_maximum_deposit(env: &Env) -> Result<U256, Error> {
670 env.storage()
671 .persistent()
672 .get(&DataKey::MaximumDepositAmount)
673 .ok_or(Error::NotInitialized)
674 }
675
676 /// Get the verifier contract address
677 fn get_verifier(env: &Env) -> Result<Address, Error> {
678 env.storage()
679 .persistent()
680 .get(&DataKey::Verifier)
681 .ok_or(Error::NotInitialized)
682 }
683
684 /// Convert a U256 into a 32-byte big-endian field element
685 fn u256_to_bytes(env: &Env, v: &U256) -> BytesN<32> {
686 let mut buf = [0u8; 32];
687 v.to_be_bytes().copy_into_slice(&mut buf);
688 BytesN::from_array(env, &buf)
689 }
690
691 /// Get the admin address
692 fn get_admin(env: &Env) -> Result<Address, Error> {
693 env.storage()
694 .persistent()
695 .get(&DataKey::Admin)
696 .ok_or(Error::NotInitialized)
697 }
698
699 /// Get the latest root of the Merkle tree that defines the pool
700 pub fn get_root(env: &Env) -> Result<U256, Error> {
701 Ok(MerkleTreeWithHistory::get_last_root(env)?)
702 }
703
704 /// Update the contract administrator
705 ///
706 /// Transfers administrative control to a new address. Requires
707 /// authorization from the current admin.
708 ///
709 /// # Arguments
710 ///
711 /// * `env` - The Soroban environment
712 /// * `new_admin` - New address that will have administrative permissions
713 pub fn update_admin(env: Env, new_admin: Address) -> Result<(), Error> {
714 if !env.storage().persistent().has(&DataKey::Admin) {
715 return Err(Error::NotInitialized);
716 }
717 soroban_utils::update_admin(&env, &DataKey::Admin, &new_admin);
718 Ok(())
719 }
720
721 // ========== ASP Contract Functions ==========
722
723 /// Get the ASP Membership contract address
724 fn get_asp_membership(env: &Env) -> Result<Address, Error> {
725 env.storage()
726 .persistent()
727 .get(&DataKey::ASPMembership)
728 .ok_or(Error::NotInitialized)
729 }
730
731 /// Get the ASP Non-Membership contract address
732 fn get_asp_non_membership(env: &Env) -> Result<Address, Error> {
733 env.storage()
734 .persistent()
735 .get(&DataKey::ASPNonMembership)
736 .ok_or(Error::NotInitialized)
737 }
738
739 /// Update the ASP Membership contract address
740 ///
741 /// Changes the ASP Membership contract address. Requires admin
742 /// authorization.
743 ///
744 /// # Arguments
745 ///
746 /// * `env` - The Soroban environment
747 /// * `new_asp_membership` - New ASP Membership contract address
748 pub fn update_asp_membership(env: &Env, new_asp_membership: Address) -> Result<(), Error> {
749 let admin = Self::get_admin(env)?;
750 admin.require_auth();
751 env.storage()
752 .persistent()
753 .set(&DataKey::ASPMembership, &new_asp_membership);
754 Ok(())
755 }
756
757 /// Update the ASP Non-Membership contract address
758 ///
759 /// Changes the ASP Non-Membership contract address. Requires admin
760 /// authorization.
761 ///
762 /// # Arguments
763 ///
764 /// * `env` - The Soroban environment
765 /// * `new_asp_non_membership` - New ASP Non-Membership contract address
766 pub fn update_asp_non_membership(
767 env: &Env,
768 new_asp_non_membership: Address,
769 ) -> Result<(), Error> {
770 let admin = Self::get_admin(env)?;
771 admin.require_auth();
772 env.storage()
773 .persistent()
774 .set(&DataKey::ASPNonMembership, &new_asp_non_membership);
775 Ok(())
776 }
777
778 /// Get the current Merkle root from the ASP Membership contract
779 ///
780 /// Makes a cross-contract call to retrieve the current root of the
781 /// membership Merkle tree.
782 ///
783 /// # Arguments
784 ///
785 /// * `env` - The Soroban environment
786 ///
787 /// # Returns
788 ///
789 /// The current membership Merkle root as U256
790 pub fn get_asp_membership_root(env: &Env) -> Result<U256, Error> {
791 let asp_address = Self::get_asp_membership(env)?;
792 let client = ASPMembershipClient::new(env, &asp_address);
793 Ok(client.get_root())
794 }
795
796 /// Get the current Merkle root from the ASP Non-Membership contract
797 ///
798 /// Makes a cross-contract call to retrieve the current root of the
799 /// non-membership Sparse Merkle tree.
800 ///
801 /// # Arguments
802 ///
803 /// * `env` - The Soroban environment
804 ///
805 /// # Returns
806 ///
807 /// The current non-membership Merkle root as U256
808 pub fn get_asp_non_membership_root(env: &Env) -> Result<U256, Error> {
809 let asp_address = Self::get_asp_non_membership(env)?;
810 let client = ASPNonMembershipClient::new(env, &asp_address);
811 Ok(client.get_root())
812 }
813}