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.
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 infund: 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 implementsCustomAccountInterface, 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.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.FAQ
Can any of these contracts be upgraded or paused?
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.Does anything on-chain reveal a payout amount?
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.
Who controls the vault holding my USDC?
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 for how the vault fits the
deposit flow.