> ## Documentation Index
> Fetch the complete documentation index at: https://docs.confiroll.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Soroban contracts

> payslip-anchor, cctp-vault, and passkey-wallet: what each does, its functions, its storage and events, and how it's used. All deployed on testnet.

Confiroll ships three purpose-built Soroban contracts (Rust, `soroban-sdk` 27, `#![no_std]`,
immutable, with no admin/upgrade/pause). The confidential *token* itself is the OpenZeppelin
Confidential Token; these three surround it.

<Info>
  All three contracts store **zero secrets**, and the payslip anchor stores **zero amounts**.
  Only `CctpVault` holds value, and it is honestly custodial **to its owner** (the employer),
  never to Confiroll.
</Info>

## PayslipAnchor

**Why it exists:** to make Confiroll's off-chain, end-to-end-encrypted payslip documents
tamper-evident and to record viewing-key rotations, without ever putting a payslip's contents
or an amount on-chain.

**Purpose:** tamper-evidence for Confiroll's off-chain, end-to-end-encrypted payslip
documents, plus an auditable viewing-key-rotation record. It stores only 32-byte digests,
never ciphertext, plaintext, or amounts.

Confiroll hashes one payroll cycle's N encrypted payslips into a padded binary **Merkle tree**
and writes only the **root** on-chain, so anchoring costs the same whether you pay 3 people or
300\.

| Function                                          | What it does                                                                                                                                                              |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `anchor_cycle(employer, cycle_id, root, count)`   | Anchors a cycle's Merkle root; the same employer can supersede it (bumping a monotonic epoch and emitting the previous root). Rejects empty cycles and foreign employers. |
| `verify_inclusion(cycle_id, digest, index, path)` | Read-only proof that a specific payslip is in the anchored cycle; a fixed proof depth blocks forgery.                                                                     |
| `publish_key(owner, role, key_id)`                | Records a viewing-key rotation ceremony (stores `sha256(pubkey)`), bumping an epoch on change.                                                                            |
| `get_cycle` / `get_key`                           | Read the stored cycle / key record.                                                                                                                                       |

**Storage and events:** the durable record is the **event stream** (`CycleAnchored`,
`CycleSuperseded`, `KeyPublished`, `KeyRotated`); stored state exists only to enforce
first-write-wins ownership and a monotonic epoch. Each stored cycle keeps its 32-byte root and
count, never contents. The contract recomputes leaf and node hashes in-contract with
domain-separation tags.

<Warning>
  The anchor is **tamper-evident, not tamper-proof**, and it does **not** hide amounts or make
  the graph private. Amount-hiding is the confidential token's job. (The source comments state
  exactly this.)
</Warning>

## CctpVault

**Why it exists:** a contract can hold USDC through the asset contract with **no trustline and
no XLM reserve**, so the vault is the landing point for CCTP-bridged USDC and the one-call
bridge from that public balance into the confidential token.

**Purpose:** the C-address landing point for CCTP-bridged USDC, and the bridge from that public
balance into the confidential token. It exists because a contract can hold USDC through the
asset contract with **no trustline and no XLM reserve**, whereas minting to a classic account
would revert without a pre-provisioned trustline.

| Function                                       | What it does                                                                                                                                             |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `__constructor(owner, token, underlying)`      | Fixes the owner (employer), the confidential token address, and the USDC asset at deploy.                                                                |
| `fund(to, amount)`                             | Owner-only. Deposits vault-held USDC into `to`'s confidential balance, pre-authorizing exactly the nested asset transfer the token's `deposit` performs. |
| `sweep(to, amount)`                            | Owner-only escape hatch to move public USDC out without entering the confidential token.                                                                 |
| `balance()` / `owner` / `token` / `underlying` | Read state.                                                                                                                                              |

**Storage and events:** the `owner`, `token`, and `underlying` are fixed at construction and
never change; the vault holds only its public USDC balance and no secrets. There is
deliberately **no `transfer_ownership`** (it would be attack surface), so a new owner means a
fresh vault plus a `sweep` migration.

### The nested-auth boundary

The important behavior is in `fund`: the token's `deposit` performs a nested asset `transfer`
one level deeper than Soroban's automatic invoker-auth reaches. Soroban authorizes the direct
call you make, but not a sub-call that the callee makes on your behalf. So the vault explicitly
authorizes *exactly* that sub-call, scoped to contract, asset, recipient, and amount, and
nothing broader. Authorizing the precise nested transfer (rather than granting the token
blanket authority over the vault) is what keeps `fund` from being an over-broad approval.

## PasskeyWallet

**Why it exists:** to give users with no wallet at all a **seedless** Soroban contract account,
authorized by a WebAuthn passkey (Face ID or a security key), with no seed phrase and no
extension.

**Purpose:** a **seedless Soroban contract account** (C-address) authorized by a WebAuthn
passkey (Face ID / a security key), with no seed phrase and no extension. It implements
`CustomAccountInterface`, so `require_auth()` on the contract runs its `__check_auth`, and the
confidential token's `from.require_auth()` accepts it exactly like a classic account.

| Function                                          | What it does                                                                                                         |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `__check_auth(payload, signature, auth_contexts)` | Verifies a WebAuthn assertion against the stored P-256 key: challenge binding, digest recompute, `secp256r1_verify`. |
| `public_key` / `credential_id`                    | Read the stored P-256 public key and its WebAuthn credential id.                                                     |

`__check_auth` does three things, in order:

<Steps>
  <Step title="Challenge binding">
    Confirms the transaction's signature payload is the exact challenge embedded in the
    WebAuthn `clientDataJSON`. Confiroll base64url-encodes the expected value and requires it to
    terminate at the closing quote, which defeats the classic passkey-wallet replay bug (the
    contract rejects a signature over a *different* challenge that merely shares a prefix).
  </Step>

  <Step title="Recompute the signed digest">
    <code>sha256(authenticatorData || sha256(clientDataJSON))</code>, what the authenticator
    actually signs.
  </Step>

  <Step title="Verify">
    <code>secp256r1\_verify</code> (CAP-51) against the stored P-256 public key.
  </Step>
</Steps>

**Storage:** one P-256 public key and its credential id, both read-only after construction.
Deliberate limits: **one credential per wallet**, no recovery, no policy engine, immutable.

<Note>
  The passkey wallet is proven for `register` on the confidential token. It provides a
  seedless contract account authorized by a WebAuthn passkey, and it runs as an independent
  component alongside Stellar Wallets Kit, covering users with no wallet at all.
</Note>

## Testnet deployments

These are public on-chain identifiers. Verify them yourself on
[stellar.expert (testnet)](https://stellar.expert/explorer/testnet).

| Contract                            | Testnet address                                            |
| ----------------------------------- | ---------------------------------------------------------- |
| PayslipAnchor                       | `CBHXTGA3NU3BJYUODMH2XJNUPPJOJV5N5XDKZBQ6EO2LRNKUPTF3RNUE` |
| CctpVault (employer-owned)          | `CCXPSER3ZB5JBUETTY3OZXAXFYFKLEPF33JOWTLVLWQBXHHQ5CKDRIKB` |
| PasskeyWallet (register-only)       | `CDMFC5I7E3HPTRDNWEHZFPU7CFRT7TBX3P53I7X5AM7AV7HYSADUQ4HW` |
| Confidential token (over CCTP USDC) | `CB4OERSZN2O2GFWPX5ZDBYTLKPLBEF5CUNA63O64QTUBOMO4AYLC5AI2` |

### Verify it yourself

<Steps>
  <Step title="Open the testnet explorer">
    Go to [stellar.expert/explorer/testnet](https://stellar.expert/explorer/testnet).
  </Step>

  <Step title="Paste an address">
    Paste one of the four addresses above into the search box. Each resolves to a deployed
    contract on the Stellar testnet.
  </Step>

  <Step title="Read the on-chain record">
    Inspect the contract's invocations and, for the anchor, its `CycleAnchored` and
    `KeyPublished` events. You are reading public chain data, so nothing here reveals a
    confidential amount.
  </Step>
</Steps>

Each contract ships a self-verifying Rust test suite covering its security-critical paths.
The tests exercise the vault's nested-auth boundary and the passkey's replay guard through the
real Soroban host auth path, not mocked.

## FAQ

<AccordionGroup>
  <Accordion title="Can any of these contracts be upgraded or paused?">
    No. All three are immutable, built on `soroban-sdk` 27 with `#![no_std]`, and ship with no
    admin, upgrade, or pause entry point. There is no `transfer_ownership` on the vault either,
    so ownership changes mean deploying a fresh vault and migrating with `sweep`.
  </Accordion>

  <Accordion title="Does anything on-chain reveal a payout amount?">
    No. The payslip anchor stores only 32-byte Merkle roots and key digests, never contents or
    amounts. Amount-hiding is the confidential token's job. The deposited total and the
    withdrawn amount are public by design; the per-recipient split and ongoing balances are
    hidden. The transaction graph (from, to, when) stays public, so this is confidentiality,
    not anonymity.
  </Accordion>

  <Accordion title="Who controls the vault holding my USDC?">
    The employer. `CctpVault` is custodial to its `owner`, and that owner is set to the employer
    at construction, never a Confiroll key. Only the owner can call `fund` or `sweep`, and the
    vault stores zero secrets. See [funding](/developers/funding) for how the vault fits the
    deposit flow.
  </Accordion>
</AccordionGroup>
