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}