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

System Overview

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.

transfer flow
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 Stellar

Deposit flow

deposit 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.

System Overview

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

ModeWho verifiesTrust requirementLatency
onchain (default)Fairyring zkp moduleNone: fully trustlessHigher (cross-chain round trip)
Off-chain (use_offchain_verify=true)FairyPort Rust FFI libraryTrust the relayer's verificationLower (proof checked before relay)