Skip to content

Contract Flow

This page walks through the on-chain contract lifecycle of a Privacy Channel: the exact contract calls behind setup, deposit, transfer, and withdrawal. It is the contract-level companion to Deposit and Withdrawal Flows, which covers the same operations from the user and provider perspective.

Components

  • privacy-channel contract: the vault. Custodies the SAC asset, tracks supply, and stores UTXOs. A UTXO is a BytesN<65> secp256r1 public key plus an amount; the matching secret key owns it.
  • channel-auth contract: a Soroban custom account. Its __check_auth gates every bundle, and it holds the provider registry.
  • Single state-changing entry: transact(op: ChannelOperation), where op = { spend, create, deposit, withdraw }.

Setup (one-time)

  1. Deploy channel-auth; its owner is the council (set via ownable at construction).
  2. The council calls add_provider(provider_pubkey) on channel-auth (owner-gated by ownable::enforce_owner_auth, emits ProviderAdded). Without this, require_provider returns ProviderThresholdNotMet and nothing can move.
  3. Deploy privacy-channel via __constructor(admin, auth_contract, asset), binding it to that channel-auth and the SAC asset (one channel per asset).

Deposit (Alice puts money in)

  1. op: deposit=[(Alice, amt, [Create(U1, amt)])], create=[(U1, amt)]; spend/withdraw empty.
  2. Provider submits transact(op).
  3. process_bundle calls auth().require_auth_for_args(req) -> __check_auth -> require_provider (provider Ed25519, threshold 1). No spends, so handle_utxo_auth checks nothing.
  4. process_bundle runs store.create(U1, amt); asserts Σdeposit == Σcreate.
  5. execute_external_operations: Alice.require_auth_for_args([conds]), asset.transfer(Alice -> contract, amt), increase_supply(amt).

Send (Alice pays Bob)

  1. Bob derives U_bob, gives Alice its pubkey.
  2. op: spend=[(U_alice, [Create(U_bob, amt), Create(U_change, chg)])], create=[(U_bob, amt),(U_change, chg)].
  3. Provider submits transact(op).
  4. __check_auth: require_provider + handle_utxo_auth verifies Alice's secp256r1 signature over U_alice's burn payload ("BURN" || pubkey || amount).
  5. assert_signed_effects_are_executed (MOON-01): the signed Create conditions equal the executed creates.
  6. process_bundle: store.spend(U_alice), store.create(U_bob), store.create(U_change); asserts Σspent == Σcreate. No SAC call, supply unchanged.

Receive (Bob)

  1. No transact. U_bob was created in Alice's bundle; Bob holds an unspent UTXO under his key and spends it later by producing the secp256r1 signature for U_bob.

Withdraw (Bob cashes out)

  1. op: spend=[(U_bob, [ExtWithdraw(Bob, amt), Create(U_change, chg)])], withdraw=[(Bob, amt, conds)], create=[(U_change, chg)].
  2. Provider submits transact(op).
  3. __check_auth: require_provider + handle_utxo_auth (Bob's secp256r1 over U_bob).
  4. assert_signed_effects_are_executed (MOON-01): signed ExtWithdraw + Create equal the executed withdraw + create.
  5. process_bundle: store.spend(U_bob), store.create(U_change); asserts Σspent == Σwithdraw + Σcreate.
  6. execute_external_operations: authorize_as_current_contract(...), asset.transfer(contract -> Bob, amt), decrease_supply(amt).

Invariant

Across every bundle: Σdeposit + Σspent == Σcreate + Σwithdraw, every spend carries the owner's secp256r1 signature, and a registered provider co-signs the whole thing.