System Design
Architecture
Stellar Stabletrust is a two-contract system bridged by a cross-chain relayer. Stellar handles user authentication and token custody; Fairyring handles all cryptographic state.
System overview
The system consists of three components that work in concert:
Stellar
Soroban Contract
- User authentication (require_auth)
- Token escrow (custody)
- Intent capture & event emission
- State finalization on callback
- Encrypted balance storage per token
Off-chain (Go)
FairyPort Relayer
- WebSocket event monitoring (both chains)
- Optional ZK proof pre-verification (Rust FFI)
- Cross-chain transaction signing
- LevelDB checkpoint persistence
- Retry and dead-letter management
Fairyring
CosmWasm Contract
- Whitelist guard (relayer allowlist)
- Replay protection (tx_id)
- Homomorphic ElGamal arithmetic
- onchain ZKP verification
- Multi-denom encrypted balance store
System diagram

Encryption model
All balances are stored as twisted ElGamal ciphertexts on the Ristretto255 elliptic curve. A ciphertext for a value m under public key PK with random scalar r is:
c1 = r · G
c2 = r · PK + m · G
This construction is additively homomorphic: adding two ciphertexts produces a ciphertext of the sum, enabling the Fairyring contract to compute encrypted balance updates without decrypting any amounts.
Enc(a) + Enc(b) = Enc(a + b): used for apply_pending
Enc(a) − Enc(b) = Enc(a − b): used for sender on transfer
Enc(a) + b = Enc(a + b): used for deposit (plaintext)
Enc(a) − b = Enc(a − b): used for withdraw (plaintext)
Transfer flow (step-by-step)
Every action follows a request → relay → execute → callback pattern. The Stellar contract captures intent; Fairyring executes the cryptographic logic; FairyPort bridges both.
Step 1: User initiates on Stellar
SDK calls transfer_confidential(sender, recipient, token, proof)
Soroban contract: validates auth, checks fee, stores PendingTransfer
Event emitted: TransferRequested(sender, recipient, tx_id, proof, pubkeys, avail_c1/c2)
Step 2: FairyPort detects via WebSocket
Soroban decoder parses event into a typed Go struct
Step 3: Proof routing
If use_offchain_verify=true → OffchainQueue → Rust FFI VerifyTransferPacked()
Invalid? → send error callback to Stellar; skip
Valid? → re-enqueue for Cosmos dispatch
If use_offchain_verify=false → pass proof data directly to Fairyring for onchain ZKP
Step 4: Fairyring transaction
FairyPort builds MsgExecuteContract { transfer_confidential { ... } }
Signs with relayer key, broadcasts via gRPC
Step 5: Fairyring execution
Contract verifies proof (onchain or trusts off-chain result)
Sender available balance -= encrypted_amount (homomorphic subtract)
Recipient pending balance += encrypted_amount (homomorphic add)
PendingTransfer record saved; credit_id incremented
Wasm event emitted: action=transfer_confidential, ciphertext attributes
Step 6: Callback to Stellar
FairyPort parses Wasm event, builds Soroban invocation
Calls transferConfidentialResponse(sender, recipient, tx_id, status, ciphertexts)
Stellar contract updates Available and Pending ciphertext storage
Step 7: Recipient applies pending
Recipient calls apply_pending() on Stellar
FairyPort relays to Fairyring apply_pending
Fairyring: pending_balance merged into available_balance
Callback confirms new available ciphertext on StellarDeposit flow
Step 1: User calls deposit(owner, token, plain_amount)
Token transferred from owner into Soroban contract escrow
PendingDeposit stored with current available ciphertext (c1/c2)
Event: DepositRequested(owner, token, tx_id, plain_amount, avail_c1, avail_c2)
Step 2: FairyPort detects, builds Fairyring deposit message
Payload: { owner, items: [{denom, plain_amount}], tx_id, available_c1, available_c2 }
Step 3: Fairyring processes deposit
add_plain_to_cipher(available[denom], plain_amount)
Updates encrypted available balance (c1 += plain_amount * G)
Event: action=deposit, new_avail_c1, new_avail_c2
Step 4: Callback to Stellar
depositResponse(owner, token, tx_id, ok, new_avail_c1, new_avail_c2)
Stellar stores updated ciphertext in Available(owner, token)Two-phase pending balance
Incoming transfers do not immediately become spendable. They land in the recipient's pending balance and must be explicitly merged via apply_pending.
available_balance
Immediately spendable. Updated by deposit, withdraw, and after apply_pending. Each denom has its own ElGamal ciphertext slot.
pending_balance
Accumulates incoming transfers homomorphically. Cleared and merged into available on apply_pending. Prevents double-spend race conditions.

Replay protection
Each account tracks a monotonically increasing last_processed_id on Fairyring. Any incoming message with a tx_id less than or equal to the stored value is handled as a duplicate: the contract reconstructs the prior response from current state without re-running cryptographic operations. This is critical for FairyPort restart recovery without double-applying state changes.
Proof verification modes
| Mode | Who verifies | Trust requirement | Latency |
|---|---|---|---|
| onchain (default) | Fairyring zkp module | None: fully trustless | Higher (cross-chain round trip) |
| Off-chain (use_offchain_verify=true) | FairyPort Rust FFI library | Trust the relayer's verification | Lower (proof checked before relay) |
