Skip to main content
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.
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.

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

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. 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. __check_auth does three things, in order:
1

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).
2

Recompute the signed digest

sha256(authenticatorData || sha256(clientDataJSON)), what the authenticator actually signs.
3

Verify

secp256r1_verify (CAP-51) against the stored P-256 public key.
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.
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.

Testnet deployments

These are public on-chain identifiers. Verify them yourself on stellar.expert (testnet).

Verify it yourself

1

Open the testnet explorer

2

Paste an address

Paste one of the four addresses above into the search box. Each resolves to a deployed contract on the Stellar testnet.
3

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

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.
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.
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 for how the vault fits the deposit flow.