Architecture

Architecture

A Soroban contract on testnet, a snarkjs circuit, three Node services, and this Next.js frontend. The private path runs entirely in the browser; the services hold only issuer-side state.

PoolPass system architecture

Contracts

RoleContract ID
PoolPassCBVARDGO…M7K2FV
Testnet USDC (SAC)CAX5YVXG…I4KP77
Pool tokenCBUXSNRS…VY2KWC
Poseidon parity gateCDBKFLR5…VIFUWD
Groth16 parity gateCALDSMCD…EI6UFX

PoolPass methods

contract interfaceinitialize(issuer, usdc_sac, pool_token, groth16_vk: Bytes, merkle_depth: u32, pool_name: String) -> Result<(), Error>
update_accredited_set(issuer, leaf_hashes: Vec<BytesN<32>>) -> Result<BytesN<32>, Error>
subscribe(investor, amount: i128, proof: Bytes, public_inputs: Vec<BytesN<32>>) -> Result<BytesN<32>, Error>
get_pool_info() -> Result<PoolInfo, Error>
advance_epoch(issuer) -> Result<u32, Error>

The issuer must authorize set updates and epoch advances. The investor must authorize subscribe and its nested Testnet USDC transfer.

Events

PoolPass uses the SDK 26 #[contractevent] API. The generated publish prepends the snake-case event-name symbol to the topic vector; non-topic fields are a canonical ScMap, named rather than positional. Reproduced verbatim from the integration contract:

event shapesRootUpdated { root, leaf_count, epoch(topic), timestamp }
  topics: Symbol("root_updated"), epoch: u32
  data:   { leaf_count: u32, root: BytesN<32>, timestamp: u64 }   // key order: leaf_count, root, timestamp

Subscribed { investor(topic), amount, nullifier, commitment, epoch(topic), timestamp }
  topics: Symbol("subscribed"), investor: Address, epoch: u32
  data:   { amount: i128, commitment: BytesN<32>, nullifier: BytesN<32>, timestamp: u64 }  // key order: amount, commitment, nullifier, timestamp

EpochAdvanced { epoch(topic), timestamp }
  topics: Symbol("epoch_advanced"), epoch: u32
  data:   { timestamp: u64 }

For RPC getEvents, filter by type: "contract", the PoolPass contract ID, and the encoded first topic via nativeToScVal(name, { type: "symbol" }).toXDR("base64").

Services

ServiceRole
apiFastify HTTP: /accredit, /faucet, /verify, /prove (fallback), /pools, /pool/:id.
indexerReads RPC getEvents, dedupes by event ID, persists to services/data/events.json.
demo-issuerHolds the issuer key; commits leaves and mints Testnet USDC.

On-chain Merkle recomputation

The Groth16 proof already outputs the merkle_root as a public input, so in principle the contract could skip on-chain recomputation and just compare the proof’s root to the stored root. PoolPass deliberately recomputes the root on-chain as well, using Stellar’s native Poseidon host function, for two reasons:

Why recompute?

(1) It demonstrates that Stellar’s Poseidon implementation agrees with the off-chain and circuit implementations. This is the Gate A parity evidence.

(2) It provides an independent integrity check that the Merkle tree the contract stores matches the one the proof was generated against.

In a production deployment optimized for gas, the recomputation could be removed and the contract would rely solely on the proof’s public root input. The current design prioritizes parity verification over gas efficiency.

Repository

layoutcontracts/        Soroban contracts (poolpass, mock token, gates)
circuits/         Circom circuit, artifacts, fixtures, VK
src/              shared crypto: poseidon, field, merkle, groth16 serializer
services/         api, indexer, demo-issuer
merkle-tools/     standalone tree + proof helpers
web/              this Next.js frontend
deployments.json  machine-readable source of truth