From 34bf79446ee4be27ea94d6f94e39155c9c2858d3 Mon Sep 17 00:00:00 2001
From: brozorec <9572072+brozorec@users.noreply.github.com>
Date: Tue, 6 Oct 2026 14:42:24 +0200
Subject: [PATCH] Stellar: add confidential token docs
Add docs for the confidential token (stellar-confidential crate): overview
and installation, operations, spenders, auditing, compliance, clawback,
proof system, wallet integration, indexing, and selective disclosure.
Register the pages in the Stellar navigation and list the confidential
and ZK Email error-code ranges.
---
content/stellar-contracts/index.mdx | 12 +-
.../tokens/confidential/auditing.mdx | 237 ++++++++++
.../tokens/confidential/clawback.mdx | 197 ++++++++
.../tokens/confidential/compliance.mdx | 446 ++++++++++++++++++
.../tokens/confidential/confidential.mdx | 279 +++++++++++
.../tokens/confidential/indexing.mdx | 146 ++++++
.../tokens/confidential/operations.mdx | 255 ++++++++++
.../tokens/confidential/proof-system.mdx | 302 ++++++++++++
.../confidential/selective-disclosure.mdx | 151 ++++++
.../tokens/confidential/spenders.mdx | 201 ++++++++
.../confidential/wallet-integration.mdx | 203 ++++++++
src/navigation/stellar.json | 74 +++
12 files changed, 2501 insertions(+), 2 deletions(-)
create mode 100644 content/stellar-contracts/tokens/confidential/auditing.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/clawback.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/compliance.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/confidential.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/indexing.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/operations.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/proof-system.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/selective-disclosure.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/spenders.mdx
create mode 100644 content/stellar-contracts/tokens/confidential/wallet-integration.mdx
diff --git a/content/stellar-contracts/index.mdx b/content/stellar-contracts/index.mdx
index 0bb78d77..6df8c0f2 100644
--- a/content/stellar-contracts/index.mdx
+++ b/content/stellar-contracts/index.mdx
@@ -16,6 +16,7 @@ for access control and contract management.
* **[Non-Fungible Tokens (NFTs)](/stellar-contracts/tokens/non-fungible/non-fungible)**: Unique digital assets with verifiable ownership.
* **[Real World Assets (RWAs)](/stellar-contracts/tokens/rwa/rwa)**: Digital assets representing real-world assets.
* **[Vault](/stellar-contracts/tokens/vault/vault)**: Digital assets representing a fixed or dynamic supply of identical units.
+* **[Confidential Token](/stellar-contracts/tokens/confidential/confidential)**: SEP-41 wrapper with private balances and transfer amounts, verified on-chain with zero-knowledge proofs.
## Access Control
@@ -51,7 +52,8 @@ we use the following convention:
* RWA: `3XX`
* Vault: `4XX`
-Any future tokens will continue from `5XX`, `6XX`, and so on.
+Any future tokens will continue from `5XX`, `6XX`, and so on. The [Confidential Token](/stellar-contracts/tokens/confidential/confidential)
+is the exception: it is split across four modules and uses the four-digit codes listed below.
Similarly, utilities and other modules have their own error codes:
@@ -65,9 +67,15 @@ Similarly, utilities and other modules have their own error codes:
* Access Control: `20XX`
* Ownable: `21XX`
* Role Transfer (internal common module for 2-step role transfer): `22XX`
-* Accounts: `3XXX`
+* Accounts: `30XX`–`32XX`
+* Confidential Token: `33XX`–`36XX`
+ * Auditor: `33XX`
+ * Verifier: `34XX`
+ * Token: `35XX`
+ * Compliance: `36XX`
* Governance: `4XXX`
* Fee Abstraction: `5XXX`
+* ZK Email: `6XXX`
## Important Notes
As a deliberate design choice, this library manages the TTL for temporary and persistent storage items.
diff --git a/content/stellar-contracts/tokens/confidential/auditing.mdx b/content/stellar-contracts/tokens/confidential/auditing.mdx
new file mode 100644
index 00000000..8b07823b
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/auditing.mdx
@@ -0,0 +1,237 @@
+---
+title: Auditing
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/src/auditor)
+
+The Confidential Token hides balances and amounts from the public, but not from everyone. Every account is bound
+to an **auditor**, and each operation that moves private value encrypts the relevant figures to that auditor's key.
+The zero-knowledge proof of the operation checks this encryption, so it cannot be skipped or faked, and the
+account holder never has to take a separate step.
+
+This page covers what auditors can see, how auditor keys are managed, and what an auditor operator has to run
+off-chain.
+
+## Dual-Auditor Model
+
+Each account picks an `auditor_id` when it calls `register`. A confidential transfer then produces two sets of
+ciphertexts in the same transaction:
+
+- One for the **recipient's auditor**, the auditor bound to the receiving account.
+- One for the **sender's auditor**, the auditor bound to the sending account (for a spender transfer, the owner's
+ auditor).
+
+Withdrawals and `set_spender` produce ciphertexts for the sender's (owner's) auditor only. A spender's own auditor
+receives nothing, either at `set_spender` or on spender transfers. Deposits, merges, `revoke_spender` and clawbacks
+produce none: a deposit amount is already public, and the others only combine existing commitments (a clawback also
+subtracts a public amount).
+
+Each auditor decrypts its ciphertexts from the public event using its own secret key, the ephemeral public key and
+the per-operation salt published in that event (for a spender transfer, the new allowance salt). The one exception
+is the allowance blinding escrowed at `set_spender`, which is masked with the spender's address from the event
+topic instead of the salt. Decryption needs no viewing key, no cooperation from the account holder and no extra
+on-chain read.
+
+Auditing is scoped to accounts: an auditor learns only about operations that involve accounts bound to its
+`auditor_id`. That includes the amount of every transfer between those accounts and counterparties bound to other
+auditors.
+
+### Auditor Binding
+
+The `auditor_id` is stored in the account's `ConfidentialAccount` entry and **cannot be changed** after
+registration. The core contract only checks that the id exists in the auditor registry. Any auditor in a shared
+registry is therefore a valid choice unless the deployment restricts it. To allow only designated auditors,
+override `on_register`, as shown in [Restrict Auditor Selection](/stellar-contracts/tokens/confidential/compliance#restrict-auditor-selection).
+
+Only the **key** registered under an `auditor_id` can change, through rotation (see
+[Key Rotation](#key-rotation)).
+
+## What Each Auditor Sees
+
+| Data | Recipient's auditor | Sender's (owner's) auditor |
+| ---- | ------------------- | -------------------------- |
+| Transfer amount (transfers and spender transfers) | Yes | Yes |
+| Per-transfer blinding factor `r_transfer` | Yes | No |
+| Sender's post-transfer balance | No | Yes |
+| Post-withdrawal balance | n/a | Yes (the withdrawal amount itself is public) |
+| Spendable-balance blinding after a withdrawal, outgoing transfer, or `set_spender` | No | Yes |
+| Escrowed allowance and post-escrow balance at `set_spender` | n/a | Yes |
+| Allowance blinding at `set_spender` | n/a | Yes |
+| Post-transfer allowance and its blinding (spender transfers) | No | Yes |
+
+The recipient's auditor never sees the sender's balance. The [auditing specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/auditing.md#auditor-visibility-properties)
+gives the exact ciphertext layout for each event.
+
+### Opening Capabilities
+
+Balances are Pedersen commitments `C = v·G + r·H`. Knowing both the value `v` and the blinding `r` (the
+*opening*) means knowing the balance exactly. The table above gives each auditor enough to reconstruct an
+opening in the following cases:
+
+- **Recipient's auditor: the receiving balance between merges.** Deposits are added with zero blinding, and the
+ recipient's auditor decrypts the amount and `r_transfer` of every inbound transfer. So it can sum these to get
+ the full opening of the receiving balance. A merge folds the receiving balance into the spendable balance, and
+ this reconstruction then starts again from zero. A [clawback](/stellar-contracts/tokens/confidential/clawback)
+ resets it in the same way.
+- **Sender's auditor: the spendable balance from each checkpoint onward.** A withdrawal, an outgoing transfer, or
+ `set_spender` each hand the sender's auditor both the new spendable value and its blinding. Together these are a
+ full opening, called a *checkpoint*. An account has one auditor key for both its sender and recipient channels.
+ So an auditor that also decrypted every inbound flow can carry that opening forward through merges and
+ `revoke_spender` folds by doing the same additions the contract does.
+
+Both capabilities are **forward-only**: a key can decrypt only ciphertexts produced while it was the active key.
+Both also depend on the auditor having actually observed every event involved (see
+[Running an Auditor](#running-an-auditor)).
+
+## Spender Auditing
+
+Delegated spending is audited with the same two-auditor model:
+
+- At `set_spender`, the owner's auditor learns the escrowed amount, the owner's post-escrow balance, and the
+ blinding of the allowance commitment the operation wrote.
+- At each `confidential_transfer_from`, the recipient's auditor gets the amount and `r_transfer`, as for any
+ transfer. The owner's auditor gets the amount, the remaining allowance and the new allowance blinding.
+- `revoke_spender` publishes no auditor ciphertext. The auditor folds the allowance opening it took from the
+ delegation's last `SetSpender` or `SpenderTransfer` event back into the owner's spendable balance. A
+ [forced revocation](/stellar-contracts/tokens/confidential/clawback#forced-revocation) emits the same
+ `RevokeSpender` event and is folded the same way.
+
+Every allowance opening comes from the event that wrote that state, not from contract storage. An auditor that
+misses a delegation event cannot recover that opening later. See [Spenders](/stellar-contracts/tokens/confidential/spenders)
+for the delegation flows themselves.
+
+## The Auditor Registry
+
+Auditor public keys live in a separate contract implementing the `ConfidentialAuditor` trait. The token stores the
+registry's address at construction and calls `get_key` each time it builds the public inputs of an operation that
+encrypts to an auditor. It also calls `get_key` at `register`, to check that the id exists, and at clawback, whose
+proof takes the auditor key as a public input. Because the registry is separate:
+
+- One registry can serve many confidential tokens.
+- Keys can be registered and rotated without redeploying any token.
+- Key-management authority is separate from the token's admin powers.
+
+```rust
+pub trait ConfidentialAuditor {
+ fn register_key(e: &Env, auditor_id: u32, point: BytesN<64>, operator: Address);
+ fn rotate_key(e: &Env, auditor_id: u32, new_point: BytesN<64>, operator: Address);
+ fn get_key(e: &Env, auditor_id: u32) -> BytesN<64>; // default implementation provided
+}
+```
+
+`register_key` and `rotate_key` are privileged and have no default implementation. The implementor wires access
+control on `operator` and delegates to the matching function in `auditor::storage`. The `auditor_id` is any `u32`
+the registry operator chooses; nothing assigns it automatically. `register_key` reverts with
+`AuditorAlreadyRegistered` if the id is taken, so it never overwrites a key, and `rotate_key` reverts with
+`AuditorNotRegistered` if the id has no key. There is no function to remove a key. The two writes emit
+`AuditorRegistered` (`auditor_id`, `point`) and `AuditorRotated` (`auditor_id`, `old_point`, `new_point`).
+
+Keys are Grumpkin points encoded as 64 bytes: the `x` coordinate followed by the `y` coordinate, each 32 bytes,
+big-endian. Every write rejects the identity point (`IdentityPoint`) and any key that is not canonically encoded or
+not on the curve (`PointNotOnCurve`). An identity key would make every auditor ciphertext trivially decryptable,
+because the salt is public.
+
+Keys live in persistent storage. Each `get_key` call extends the entry's TTL to 30 days once it drops below 29
+days, so the tokens' own lookups keep an id in use alive. `register_key` and `rotate_key` don't extend it. An id
+that no token reads lasts only as long as its initial TTL unless the operator extends it, and once archived it must
+be restored before its next use.
+
+The example below gates both writes behind a `manager` role. See [Installation](/stellar-contracts/tokens/confidential/confidential#installation)
+for the crate dependency and the `alloc` feature it requires.
+
+```rust
+use soroban_sdk::{contract, contractimpl, symbol_short, Address, BytesN, Env, Symbol, Vec};
+use stellar_access::access_control::{self as access_control, AccessControl};
+use stellar_macros::only_role;
+use stellar_confidential::auditor::{storage as auditor, ConfidentialAuditor};
+
+const MANAGER_ROLE: Symbol = symbol_short!("manager");
+
+#[contract]
+pub struct ConfidentialAuditorContract;
+
+#[contractimpl]
+impl ConfidentialAuditorContract {
+ pub fn __constructor(e: &Env, admin: Address, manager: Address) {
+ access_control::set_admin(e, &admin);
+ access_control::grant_role_no_auth(e, &manager, &MANAGER_ROLE, &admin);
+ }
+}
+
+#[contractimpl(contracttrait)]
+impl ConfidentialAuditor for ConfidentialAuditorContract {
+ #[only_role(operator, "manager")]
+ fn register_key(e: &Env, auditor_id: u32, point: BytesN<64>, operator: Address) {
+ auditor::register_key(e, auditor_id, &point);
+ }
+
+ #[only_role(operator, "manager")]
+ fn rotate_key(e: &Env, auditor_id: u32, new_point: BytesN<64>, operator: Address) {
+ auditor::rotate_key(e, auditor_id, &new_point);
+ }
+}
+
+#[contractimpl(contracttrait)]
+impl AccessControl for ConfidentialAuditorContract {}
+```
+
+
+Whoever can call `rotate_key` decides who can read future auditor ciphertexts for every account bound to that
+`auditor_id`. Gate it at least as tightly as the token's own admin functions.
+
+The same applies one level up. The token's `set_auditor` storage function has no single-shot guard, so if the token
+exposes it, its caller can point every account at a different registry. For accounts whose `auditor_id` is missing
+from the new registry, every operation that needs their key, including transfers to them, then reverts with
+`AuditorNotRegistered`.
+
+
+## Key Rotation
+
+The registry shipped with the library keeps one current key per `auditor_id`, and `rotate_key` overwrites it in
+place. Production deployments can build a registry that keeps versioned keys with activation ledgers instead.
+The token and the circuits don't depend on either design: they always use whichever key the registry currently
+returns.
+
+- **In-flight proofs fail cleanly.** A proof built against the old key no longer matches the key the contract
+ fetches, so verification fails and the call reverts with no state change. The wallet rebuilds the proof against
+ the new key, with a fresh salt, and resubmits. An observer cannot link the retry to the failed attempt.
+- **The auditor keeps its old secrets.** Each historical key decrypts the events produced while it was active. The
+ registry's `AuditorRotated` event, which carries the old and the new key, is the on-chain record of each switch.
+ To decrypt an event, the auditor uses it to resolve which of its keys was active at that point and uses that
+ secret.
+- **A retired key keeps what it already opened.** Rotation does not revoke openings the old key's holder already
+ has.
+- **A new key starts with nothing.** For the spendable balance it starts over at the account's next checkpoint
+ (withdrawal, outgoing transfer, or `set_spender`). For an allowance it starts at the delegation's next
+ state-changing event. Until then it reads individual amounts but holds no full opening. Even after that
+ checkpoint, a merge that folds transfers received before the rotation, or a `revoke_spender` of a delegation last
+ written before it, breaks the spendable opening again until the next checkpoint. If the receiving balance holds
+ transfers from before the rotation, the new key can open it only after the next merge. A new-key holder therefore
+ cannot build a clawback proof for an account until it holds both openings.
+
+## Running an Auditor
+
+The library provides the `ConfidentialAuditor` trait and the `auditor::storage` helpers; a deployable registry is
+the [example contract](https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/confidential/auditor).
+It does not include an auditor client. An auditor operator has to run software that:
+
+1. **Tracks its accounts.** It indexes the token's `Register` events, whose data carries the account's
+ `auditor_id`, to learn which accounts are bound to its id and so which channel of each event is its own.
+2. **Decrypts every relevant event** with its secret key, using the ephemeral key and salt published in the event.
+3. **Maintains openings.** It replaces the spendable opening at each checkpoint, adds the amount and `r_transfer`
+ of every inbound transfer and spender transfer to the receiving opening, treats each deposit as `(amount, 0)`,
+ and folds the receiving opening into the spendable one on each `Merge`. On each `RevokeSpender` it folds in the
+ allowance opening it recorded; if it holds no record for that delegation, it marks the spendable opening
+ unavailable until the next checkpoint rather than carry a stale one. Where seizure is enabled, it applies each
+ `Clawback` as a merge minus the public amount.
+4. **Checks openings against chain state.** It recomputes the commitment from its opening and compares it with the
+ stored one. A mismatch means a missed, reordered, or pruned event, not a wrong balance.
+5. **Archives events durably.** The blindings escrowed to an auditor exist only in events, and Stellar RPC keeps
+ only a limited window of history. A durable archive is required; see
+ [Indexing and Recovery](/stellar-contracts/tokens/confidential/indexing).
+6. **Keeps every historical secret key** for as long as old events may need decrypting.
+7. **Produces clawback proofs**, where the deployment enables seizure. The auditor's secret key and the openings
+ it maintains are the witness for the [Clawback](/stellar-contracts/tokens/confidential/clawback) circuit.
+
+The client-side requirements are specified in the [Auditor Client specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/auditor-client.md).
+The full auditing model, including how each capability is bounded, is in the [auditing specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/auditing.md).
diff --git a/content/stellar-contracts/tokens/confidential/clawback.mdx b/content/stellar-contracts/tokens/confidential/clawback.mdx
new file mode 100644
index 00000000..78c4434d
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/clawback.mdx
@@ -0,0 +1,197 @@
+---
+title: Clawback
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/src/compliance)
+
+Clawback seizes value from a single frozen confidential account. It reduces the account's confidential claim by a
+public amount, proves that amount doesn't exceed what the account holds, and settles the matching underlying
+tokens over a public path. The name mirrors clawback on Stellar Classic and SAC assets, but the mechanism is
+separate: it is delivered by the opt-in `ConfidentialClawback` trait of the [compliance extension](/stellar-contracts/tokens/confidential/compliance).
+A deployment that doesn't implement the trait has no seizure capability at all.
+
+## The Pooled-Custody Problem
+
+Once a user deposits, the underlying SEP-41 ledger lists the confidential token contract as the holder of those
+funds, not the depositor. All deposits sit in one pooled balance. An issuer's SAC-level `clawback` against the
+contract's address would debit the whole pool and, with it, every holder. Seizing from one account therefore has to
+happen at the confidential layer, by reducing that account's committed claim.
+
+The contract doesn't know the target's balance: it stores only commitments. So the amount has to be checked against
+the committed values without revealing them, and without trusting the admin's word for them. That check is the job
+of the clawback proof.
+
+## Roles
+
+Seizure is coordinated between parties and no single one can carry it out alone:
+
+| Role | Decides | Does |
+| ---- | ------- | ---- |
+| **Token admin** | *Whether* to seize | Freezes the account, then authorizes `force_revoke_spender` and `clawback` through the deployment's access control. |
+| **Auditor** | *How much* and *where to* | Holds the secret key of the auditor the account is bound to, plus the openings of the account's balances. Produces the proof, which binds the amount and the destination. |
+| **Issuer** (SAC admin) | How the pool is reconciled | When the underlying is a SAC and the seizure leaves the funds in the pool, extracts the surplus with the SAC's own `clawback`. |
+
+The admin cannot produce the proof and the auditor cannot pass the admin's access check. Deployments usually give
+the seizure authority its own role, separate from the role that freezes accounts.
+
+The proof has to come from the auditor. Knowing an account's balance isn't enough. An account whose history is
+only registration, deposits, and merges has balances anyone can reconstruct from public events. The circuit
+therefore also requires the auditor's secret key, which stops anyone else from proving a seizure of such an account.
+
+## Usage
+
+`ConfidentialClawback` extends `ConfidentialCompliance`, and its two write methods have no default body, for the
+same reason. Building on the contract from the [Compliance](/stellar-contracts/tokens/confidential/compliance#usage)
+page, with a dedicated `clawback` role:
+
+```rust
+use stellar_confidential::{
+ compliance::{ClawbackData, ConfidentialClawback},
+ storage::decode_data,
+};
+
+#[contractimpl(contracttrait)]
+impl ConfidentialClawback for ConfidentialTokenContract {
+ #[only_role(operator, "clawback")]
+ fn clawback(
+ e: &Env,
+ account: Address,
+ amount: i128,
+ destination: Option
,
+ data: Bytes,
+ operator: Address,
+ ) {
+ let data: ClawbackData = decode_data(e, &data);
+ compliance::clawback(e, &account, amount, &destination, &data.proof);
+ }
+
+ #[only_role(operator, "clawback")]
+ fn force_revoke_spender(e: &Env, account: Address, spender: Address, operator: Address) {
+ compliance::force_revoke_spender(e, &account, &spender);
+ }
+}
+```
+
+The `data` argument is an XDR-encoded `ClawbackData { proof }`, not the raw proof bytes. A malformed envelope fails
+with `InvalidData`. The verifier also needs the `Clawback` key registered (see
+[Registering Verification Keys](/stellar-contracts/tokens/confidential/proof-system#registering-verification-keys)).
+
+The trait also provides `clawback_nonce(account)`, a read that returns how many seizures the account has been
+through. The auditor reads it into each new proof (see [Replay Protection](#replay-protection)).
+
+
+The freeze precondition only protects the seizure if frozen accounts are actually blocked. The trait bounds don't
+enforce this. With `type Hooks = NoHooks`, `freeze` sets a flag that nothing checks, and the target can move its
+balance before the seizure lands. The only sign is an `InvalidProof` error. A deployment that implements
+`ConfidentialClawback` has to wire `ComplianceHooks`, or a custom `Hooks` that rejects frozen accounts in every
+balance-moving callback.
+
+
+## Seizure Flow
+
+```mermaid
+sequenceDiagram
+ participant Admin as Token admin
+ participant Token as Confidential token
+ participant Auditor
+ participant Issuer as Issuer (SAC admin)
+ participant SAC as Underlying SAC
+ Admin->>Token: freeze(account)
+ Admin->>Token: force_revoke_spender(account, spender), per delegation
+ Auditor->>Token: read commitments and clawback_nonce
+ Auditor-->>Admin: proof for (amount, destination)
+ Admin->>Token: clawback(account, amount, destination, data)
+ opt destination is None
+ Issuer->>SAC: clawback(token contract, amount)
+ end
+```
+
+When `clawback` is called, after the access check on `operator`, the contract:
+
+1. **Checks preconditions.** The account must be frozen (`AccountNotFrozen`), `amount` must be strictly positive
+ (`InvalidClawbackAmount`), and `destination` must not be the token contract itself (`InvalidClawbackDestination`).
+2. **Builds the public inputs** entirely from on-chain state and the call arguments: both of the account's
+ commitments, the key of its auditor, the amount, the contract and account addresses, the destination, and the
+ account's clawback nonce.
+3. **Verifies the proof** against the clawback circuit. The proof shows that the prover holds the auditor's secret
+ key, knows the openings of both balances, and that `amount` is no more than spendable plus receiving.
+4. **Updates the balances.** The receiving balance is merged into the spendable balance and `amount` is debited, with
+ no fresh randomness. The owner and the auditor can both compute the new opening from the public `amount`, so the
+ account stays usable once it is unfrozen.
+5. **Advances the clawback nonce**, then settles the underlying (below) and emits a `Clawback` event with topics
+ `"clawback"` and `account`, and data `amount` and `destination`.
+
+Error codes are listed under [Errors](/stellar-contracts/tokens/confidential/compliance#errors).
+
+Like `force_revoke_spender`, `clawback` runs no `Hooks` callback: a freeze-aware hook would reject the very account
+it acts on.
+
+## Settlement
+
+The `destination` argument, bound into the proof by the auditor, decides what happens to the underlying tokens:
+
+| `destination` | Underlying | Pool afterwards |
+| ------------- | ---------- | --------------- |
+| `Some(d)` | Exactly `amount` is transferred to `d` in the same call. | Still equals the sum of all confidential claims. |
+| `None` | Nothing moves. | Holds `amount` more than the claims against it, until the issuer extracts the surplus. |
+
+`d` goes through no compliance check: the auditor chose it when building the proof, and the operator confirms it as
+an argument. A proof built for one destination cannot be submitted with another. The underlying's own rules still
+apply to the transfer: on a SAC, `d` has to be authorized for the asset (for a `G...` account, an authorized
+trustline), or the whole seizure reverts.
+
+`None` relies on the issuer being able to extract the surplus:
+
+- The underlying has to be a SAC whose issuer enabled clawback (`AUTH_CLAWBACK_ENABLED`) before the token
+ contract's first deposit. The SAC fixes a contract balance's clawback flag when the balance is created, so
+ enabling it later doesn't help.
+- Over native XLM or a plain SEP-41 token, nothing can extract the surplus, because the contract has no sweep
+ function. Use `Some(d)` there.
+
+
+With `destination = None`, **seize first, then extract.** The confidential seizure and the issuer's SAC `clawback`
+against the token contract's address are separate transactions, and nothing in the contract enforces their order.
+Seizing first leaves the pool briefly over-collateralized, which harms no one. Extracting first leaves it short, and
+the holders who withdraw last bear the shortfall. The shortfall becomes permanent if the seizure never goes through,
+for example because the auditor produces no proof. An issuer that extracts more than the accumulated surplus creates
+the same shortfall.
+
+
+## Forced Revocation
+
+`clawback` only sees the account's spendable and receiving balances. Allowances escrowed to spenders are out of its
+reach. `force_revoke_spender(account, spender, operator)` brings them back. On a frozen account, it performs the
+same proofless fold as the owner's `revoke_spender`, under the admin's authorization instead of the owner's. It
+emits the same `RevokeSpender` event. Expired delegations can be revoked too: expiry blocks spending, not reclaiming.
+
+Revocation changes the spendable balance, so revoke every delegation **before** the auditor builds the proof. The
+auditor's opening survives the fold only if it observed the delegation's last `SetSpender` or `SpenderTransfer`
+event (see [Auditing](/stellar-contracts/tokens/confidential/auditing#spender-auditing)).
+
+The contract keeps no list of an account's delegations. Find the spenders from the account's `SetSpender` and
+`RevokeSpender` events, and confirm each one with `get_spender_delegation`. The freeze blocks new delegations, so
+the list can't grow during the seizure. A delegation that is left out isn't seized, and its spender can use it again
+once the account is unfrozen, unless it has expired.
+
+## Replay Protection
+
+Both commitments are public inputs of the proof. Any change between building the proof and submitting it, such as an
+inbound transfer, a merge, or a revocation, makes verification fail with `InvalidProof`. The freeze holds the
+commitments still during that window. An auditor key rotation in the same window also invalidates the proof.
+
+Matching commitments alone don't prove a proof is fresh. A seizure of `amount` followed by a credit of the same
+`amount` with zero blinding can restore the exact commitments the old proof was built against. The **clawback
+nonce** closes that gap: it is part of every proof and advances on each seizure, so a proof can execute at most once.
+
+## Wallet and Auditor Consequences
+
+Owners and auditors apply a `Clawback` event as a merge followed by a public debit of `amount`. The event carries no
+encrypted balance, so it doesn't count as a checkpoint for the auditor. It resets the receiving side like a merge
+does, and the account's next withdrawal, outgoing transfer, or `set_spender` re-anchors the spendable side as usual.
+A `RevokeSpender` emitted by `force_revoke_spender` looks exactly like one initiated by the owner.
+
+Wallets, auditors, and indexers have to archive `Clawback` events with the rest of an account's history: recovering
+a balance depends on them. See [Indexing and Recovery](/stellar-contracts/tokens/confidential/indexing).
+
+The [clawback specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/compliance.md#clawback)
+covers the circuit and the contract flow in full.
diff --git a/content/stellar-contracts/tokens/confidential/compliance.mdx b/content/stellar-contracts/tokens/confidential/compliance.mdx
new file mode 100644
index 00000000..7eaaa5cd
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/compliance.mdx
@@ -0,0 +1,446 @@
+---
+title: Compliance
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/src/compliance)
+
+The compliance extension adds optional controls, configured by the deployer, on top of the Confidential Token:
+per-account freezing, passthrough of a Stellar Asset Contract's (SAC) authorization, an external authorization
+policy, and opt-in [seizure](/stellar-contracts/tokens/confidential/clawback). [Auditing](/stellar-contracts/tokens/confidential/auditing)
+is always on and needs none of this. Compliance is for deployments that also have to *restrict* who can use the
+token.
+
+A deployment with no compliance configuration pays one instance-storage read per operation and otherwise behaves
+like a plain Confidential Token.
+
+## Overview
+
+The [compliance](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/src/compliance)
+module provides:
+
+- **`ComplianceHooks`**: a ready-made [`Hooks`](/stellar-contracts/tokens/confidential/confidential#hooks) implementation
+ that runs every token entry point through the active configuration. Wire it as `type Hooks = ComplianceHooks;`.
+- **`ConfidentialCompliance`**: the admin-facing trait for `freeze`, `unfreeze`, `set_compliance_config`, and the
+ `is_frozen` and `compliance_config` reads.
+- **`ConfidentialClawback`**: the opt-in seizure trait, covered on the [Clawback](/stellar-contracts/tokens/confidential/clawback)
+ page. A deployment that doesn't implement it keeps freeze and policy checks but cannot seize funds.
+- **`Policy`**: the cross-contract interface for an external allowlist, denylist, KYC, or sanctions registry.
+- **Storage helpers** in `compliance::storage` (`gate_account`, `check_policy`, `check_sac`, and others) for
+ writing custom hooks.
+
+## Configuration
+
+All controls are driven by a single `ComplianceConfig` entry in instance storage:
+
+```rust
+pub struct ComplianceConfig {
+ pub policy: Option,
+ pub sac_passthrough: bool,
+}
+```
+
+| Field | Purpose |
+| ----- | ------- |
+| `policy` | Address of an external [policy contract](#policy-contract), or `None` for no policy check. |
+| `sac_passthrough` | When `true`, the check on every fund-holding address also asks the underlying SAC's `authorized()` view (see [SAC Authorization Passthrough](#sac-authorization-passthrough)). |
+
+The initial configuration is usually written in the constructor with `compliance::storage::set_compliance_config`.
+Later changes go through the admin-gated `set_compliance_config` entry point. It also works when no configuration
+exists yet, so compliance can be switched on after deployment. The same storage function serves both cases and
+replaces the whole configuration in one call.
+
+Until a configuration exists, `ComplianceHooks` does nothing, `is_frozen` returns `false`, `compliance_config`
+returns `None`, and `freeze` and `unfreeze` revert with `NotConfigured`. Once written, a configuration can't be
+deleted. Setting `policy: None` and `sac_passthrough: false` turns those checks off, but freezes keep applying.
+
+
+Enable `sac_passthrough` only when the underlying token is a Stellar Asset Contract. `authorized()` is part of the
+SAC admin interface, not SEP-41, so over a plain SEP-41 token every checked operation fails on the missing function.
+
+
+## Usage
+
+The write methods of `ConfidentialCompliance` have no default body, because the choice of access-control module
+is yours. Each override authorizes `operator` and then calls the matching storage function. The example below
+uses role-based access control, with separate roles for freezing and for configuration changes:
+
+```rust
+// `Bytes` and `Vec` are used by the traits' default methods.
+use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, Env, Symbol, Vec};
+use stellar_access::access_control::{self as access_control, AccessControl};
+use stellar_macros::only_role;
+use stellar_confidential::{
+ compliance::{
+ storage as compliance, ComplianceConfig, ComplianceHooks, ConfidentialCompliance,
+ },
+ storage as confidential,
+ ConfidentialAccount, // used by the trait's default methods
+ ConfidentialToken,
+ SpenderDelegation, // used by the trait's default methods
+};
+
+const FREEZER_ROLE: Symbol = symbol_short!("freezer");
+
+#[contract]
+pub struct ConfidentialTokenContract;
+
+#[contractimpl]
+impl ConfidentialTokenContract {
+ pub fn __constructor(
+ e: &Env,
+ token: Address,
+ verifier: Address,
+ auditor: Address,
+ admin: Address,
+ freezer: Address,
+ compliance_config: Option,
+ ) {
+ confidential::set_underlying_asset(e, &token);
+ confidential::set_verifier(e, &verifier);
+ confidential::set_auditor(e, &auditor);
+ confidential::set_address_as_field_element(e);
+
+ access_control::set_admin(e, &admin);
+ access_control::grant_role_no_auth(e, &freezer, &FREEZER_ROLE, &admin);
+
+ if let Some(config) = compliance_config {
+ compliance::set_compliance_config(e, &config);
+ }
+ }
+}
+
+#[contractimpl(contracttrait)]
+impl ConfidentialToken for ConfidentialTokenContract {
+ type Hooks = ComplianceHooks;
+}
+
+#[contractimpl(contracttrait)]
+impl ConfidentialCompliance for ConfidentialTokenContract {
+ #[only_role(operator, "freezer")]
+ fn freeze(e: &Env, account: Address, operator: Address) {
+ compliance::freeze(e, &account);
+ }
+
+ #[only_role(operator, "freezer")]
+ fn unfreeze(e: &Env, account: Address, operator: Address) {
+ compliance::unfreeze(e, &account);
+ }
+
+ // The admin grants the `manager` role after deployment.
+ #[only_role(operator, "manager")]
+ fn set_compliance_config(e: &Env, config: ComplianceConfig, operator: Address) {
+ compliance::set_compliance_config(e, &config);
+ }
+}
+
+#[contractimpl(contracttrait)]
+impl AccessControl for ConfidentialTokenContract {}
+```
+
+A deployment with a single compliance authority can use [Ownable](/stellar-contracts/access/ownable) and
+`#[only_owner]` instead. In that case the macro performs the check and `operator` is informational only.
+
+
+`compliance::storage::set_compliance_config` and the other storage helpers, such as `freeze`, `clawback`, and
+`force_revoke_spender`, skip authorization checks. Call them only from the constructor or from entry points that do
+their own authorization.
+
+
+## Checks
+
+When a configuration is present, `ComplianceHooks` runs each relevant address through up to three checks:
+
+1. **Freeze**: the address must not be frozen (`AccountFrozen`).
+2. **Policy**: if `policy` is set, `policy.is_authorized(address, token)` must return `true` (`NotAuthorizedByPolicy`).
+3. **SAC**: if `sac_passthrough` is enabled, the underlying SAC's `authorized(address)` must return `true` (`NotAuthorizedBySac`).
+
+Which addresses go through which checks depends on the entry point:
+
+| Entry point | Freeze | Policy | SAC |
+| ----------- | ------ | ------ | --- |
+| `register` | — | `account` | `account` |
+| `deposit` | `from`, `to` | `from`, `to` | `from`, `to` |
+| `merge` | `account` | `account` | `account` |
+| `withdraw` | `from`, `to` | `from`, `to` | `from`, `to` |
+| `confidential_transfer` | `from`, `to` | `from`, `to` | `from`, `to` |
+| `confidential_transfer_from` | `from`, `to` | `from`, `to`, `spender` | `from`, `to` |
+| `set_spender` | `account` | `account`, `spender` | `account` |
+| `revoke_spender` | `account` | `account` | `account` |
+
+`register` skips the freeze check because the account doesn't exist yet. `freeze` doesn't require registration,
+though, so an address frozen in advance can still register, but can do nothing else.
+
+### Freezing
+
+A frozen account cannot deposit, receive, transfer, withdraw, merge, or manage its spenders. Its commitments stay
+exactly as they are, so nothing can accumulate in the account and nothing can leave it until it is unfrozen. Because
+a frozen owner is also checked as `from` in `confidential_transfer_from`, freezing the owner halts its delegations
+too.
+
+The freeze targets ownership of funds, so a **spender** is never subject to the freeze or SAC checks: a spender holds
+no funds of its own. This matches the allowance models of the library's fungible and RWA tokens. Spenders are
+screened by the policy instead.
+
+Freezing is also the precondition for [clawback](/stellar-contracts/tokens/confidential/clawback). The seizure
+entry points don't run the hooks, so they can act on a frozen account.
+
+### SAC Authorization Passthrough
+
+With `sac_passthrough = true`, an issuer's own `set_authorized` / deauthorization on the underlying SAC takes
+effect at the confidential layer automatically, with no state mirrored by the token admin. The cost is one extra
+cross-contract call per checked address per operation. Like the freeze, it checks only addresses that hold funds,
+never the spender.
+
+What the SAC answers depends on the address and the asset:
+
+- **Classic account (`G...`)**: `authorized` reads the account's trustline for the asset and traps if there is
+ none. Every checked `G...` address needs a trustline, including the account at `register` and the recipient of a
+ `deposit` or `withdraw`. A missing trustline fails with the SAC's own error, not `NotAuthorizedBySac`.
+- **Contract (`C...`)**: with no SAC balance yet, `authorized` returns `true` unless the asset has
+ `AUTH_REQUIRED` set. On such an asset, the issuer has to authorize a contract account, such as a smart wallet,
+ before it can use the token.
+- **Native XLM**: `authorized` always returns `true`, so passthrough has no effect.
+
+## Policy Contract
+
+The policy is any contract implementing the `Policy` interface:
+
+```rust
+pub trait Policy {
+ fn is_authorized(e: Env, account: Address, token: Address) -> bool;
+}
+```
+
+This one boolean callback covers allowlists, denylists, and delegation to an identity registry, attestation
+provider, or sanctions oracle. List management and identity proofs stay inside the policy contract. The token
+passes its own address as `token`, so one registry can serve several confidential tokens and still apply per-token
+rules.
+
+A minimal owner-managed allowlist:
+
+```rust
+use soroban_sdk::{contract, contractimpl, Address, Env};
+use stellar_access::ownable::{self as ownable, Ownable};
+use stellar_macros::only_owner;
+use stellar_confidential::compliance::Policy;
+
+#[contract]
+pub struct AllowListPolicy;
+
+#[contractimpl]
+impl AllowListPolicy {
+ pub fn __constructor(e: &Env, owner: Address) {
+ ownable::set_owner(e, &owner);
+ }
+
+ #[only_owner]
+ pub fn set_allowed(e: &Env, account: Address, allowed: bool) {
+ // A production registry also manages the TTL of these entries.
+ if allowed {
+ e.storage().persistent().set(&account, &true);
+ } else {
+ e.storage().persistent().remove(&account);
+ }
+ }
+}
+
+#[contractimpl]
+impl Policy for AllowListPolicy {
+ fn is_authorized(e: Env, account: Address, _token: Address) -> bool {
+ e.storage().persistent().has(&account)
+ }
+}
+
+#[contractimpl(contracttrait)]
+impl Ownable for AllowListPolicy {}
+```
+
+Unlike the freeze, the policy also screens spenders. `set_spender` checks the spender when the delegation is
+granted, and `confidential_transfer_from` checks it again at spend time. `revoke_spender` deliberately doesn't check
+the spender: revocation is the owner's way out of a delegation to a spender that has since become non-compliant.
+
+The policy address can be rotated, or removed by setting it to `None`, with `set_compliance_config`. It is part of
+the deployment's trust surface. A deployment over a SAC can rely on `sac_passthrough` and skip the policy entirely.
+
+## Customizing the Hooks Trait
+
+`ComplianceHooks` is one implementation of `Hooks`. Deployments that need different behavior write their own
+implementation, usually reusing the same `compliance::storage` primitives and changing only the callbacks that need
+it. A callback only has to revert, with `panic_with_error!`, to reject an operation.
+
+
+Every `Hooks` method has an empty default body. A custom implementation has to **forward every callback it doesn't
+customize** to `ComplianceHooks` explicitly. Any callback it leaves out becomes ungated, which silently disables
+freezing for that operation and makes the clawback freeze precondition meaningless.
+
+
+### Exempt Unregistered Depositors from Screening
+
+`deposit` is the one operation where `from` may be an address that never registered: the depositor only needs to
+hold the underlying token. The default `ComplianceHooks::on_deposit` screens `from` whether or not it is registered.
+An unregistered depositor gets through if it isn't frozen, passes the policy, and, with `sac_passthrough`, is
+authorized by the SAC. Under an allowlist that lists only onboarded users, that rejects every outside payer.
+
+This implementation exempts an unregistered sender from the freeze and policy checks and keeps only the SAC check:
+
+```rust
+use soroban_sdk::{Address, Env};
+use stellar_confidential::{
+ compliance::{storage as compliance, ComplianceHooks},
+ storage::account_exists,
+ Hooks, RegisterPayload, SetSpenderPayload, SpenderTransferPayload, TransferPayload,
+ WithdrawPayload,
+};
+
+pub struct PermissiveDepositHooks;
+
+impl Hooks for PermissiveDepositHooks {
+ fn on_deposit(e: &Env, from: &Address, to: &Address, _amount: i128) {
+ let Some(config) = compliance::compliance_config(e) else {
+ return;
+ };
+ if account_exists(e, from) {
+ compliance::gate_account(e, from, &config);
+ } else if config.sac_passthrough {
+ // The SEP-41 transfer would fail anyway for a depositor the SAC
+ // has unauthorized.
+ compliance::check_sac(e, from, &config);
+ }
+ compliance::gate_account(e, to, &config);
+ }
+
+ // Every other callback keeps the default compliance behavior.
+ fn on_register(e: &Env, account: &Address, auditor_id: u32, payload: &RegisterPayload) {
+ ComplianceHooks::on_register(e, account, auditor_id, payload);
+ }
+
+ fn on_merge(e: &Env, account: &Address) {
+ ComplianceHooks::on_merge(e, account);
+ }
+
+ fn on_withdraw(
+ e: &Env,
+ from: &Address,
+ to: &Address,
+ amount: i128,
+ payload: &WithdrawPayload,
+ ) {
+ ComplianceHooks::on_withdraw(e, from, to, amount, payload);
+ }
+
+ fn on_transfer(e: &Env, from: &Address, to: &Address, payload: &TransferPayload) {
+ ComplianceHooks::on_transfer(e, from, to, payload);
+ }
+
+ fn on_spender_transfer(
+ e: &Env,
+ spender: &Address,
+ from: &Address,
+ to: &Address,
+ payload: &SpenderTransferPayload,
+ ) {
+ ComplianceHooks::on_spender_transfer(e, spender, from, to, payload);
+ }
+
+ fn on_set_spender(
+ e: &Env,
+ account: &Address,
+ spender: &Address,
+ live_until_ledger: u32,
+ payload: &SetSpenderPayload,
+ ) {
+ ComplianceHooks::on_set_spender(e, account, spender, live_until_ledger, payload);
+ }
+
+ fn on_revoke_spender(e: &Env, account: &Address, spender: &Address) {
+ ComplianceHooks::on_revoke_spender(e, account, spender);
+ }
+}
+```
+
+Wire it with `type Hooks = PermissiveDepositHooks;`. This suits deployments that accept inbound payments from
+counterparties that never register, such as an exchange wallet paying into a payroll pool. Skipping the *policy*
+check on an unregistered sender is a trade-off, not a recommendation: it also lets in an outside address that the
+policy would deny, or that the admin froze before it registered. A deployment that screens with a denylist or
+sanctions policy doesn't need this variant, because the default `ComplianceHooks` already accepts unregistered
+depositors that the policy allows.
+
+### Permit Deposits Only for Oneself
+
+Require the depositor to be the recipient, so nobody can deposit on someone else's behalf. For example, this
+prevents unsolicited "dusting" deposits that complicate auditor bookkeeping:
+
+```rust
+impl Hooks for SelfDepositOnlyHooks {
+ fn on_deposit(e: &Env, from: &Address, to: &Address, amount: i128) {
+ if from != to {
+ panic_with_error!(e, ComplianceError::NotAuthorizedByPolicy);
+ }
+ ComplianceHooks::on_deposit(e, from, to, amount);
+ }
+
+ // ...forward every other callback to ComplianceHooks, as above...
+}
+```
+
+### Restrict Auditor Selection
+
+At `register`, the account owner picks the `auditor_id` the account binds to. The core only checks that the id
+exists in the auditor registry, and `ComplianceHooks::on_register` deliberately leaves the choice open. On a shared
+registry, a deployment with designated auditors checks the choice against its own approved set:
+
+```rust
+impl Hooks for ApprovedAuditorHooks {
+ fn on_register(e: &Env, account: &Address, auditor_id: u32, payload: &RegisterPayload) {
+ let approved: Vec = e
+ .storage()
+ .instance()
+ .get(&DataKey::ApprovedAuditors)
+ .unwrap_or_else(|| Vec::new(e));
+ if !approved.contains(auditor_id) {
+ panic_with_error!(e, DeploymentError::AuditorNotApproved);
+ }
+ ComplianceHooks::on_register(e, account, auditor_id, payload);
+ }
+
+ // ...forward every other callback to ComplianceHooks, as above...
+}
+```
+
+`DataKey::ApprovedAuditors` and `DeploymentError::AuditorNotApproved` are defined by the deployment. The list is
+filled at construction or through an admin entry point. An account's `auditor_id` can never change after
+registration, so this check is the only point where a deployment controls which auditors may ever see an account's
+activity.
+
+The same mechanism supports per-deposit rate limits, allowlists keyed on the amount, writes to a separate audit
+log, or any other check that has to run atomically with the token operation. For purely observational needs,
+subscribe to the token's events instead.
+
+## Events
+
+| Event | Topics | Data | Emitted by |
+| ----- | ------ | ---- | ---------- |
+| `Frozen` | `"frozen"`, `account` | — | `freeze` |
+| `Unfrozen` | `"unfrozen"`, `account` | — | `unfreeze` |
+| `ComplianceConfigChanged` | `"compliance_config_changed"` | `policy`, `sac_passthrough` | Every `compliance::storage::set_compliance_config` call, including the one in the constructor |
+
+Seizures emit `Clawback` and, for forced revocation, `RevokeSpender` (see
+[Clawback](/stellar-contracts/tokens/confidential/clawback#seizure-flow)).
+
+## Errors
+
+| Error | Code | Raised when |
+| ----- | ---- | ----------- |
+| `NotConfigured` | 3600 | `freeze` or `unfreeze` is called before a configuration exists |
+| `AccountFrozen` | 3601 | A checked address is frozen |
+| `NotAuthorizedByPolicy` | 3602 | The policy returns `false` for a checked address |
+| `NotAuthorizedBySac` | 3603 | The SAC's `authorized` returns `false` for a checked address |
+| `AccountNotFrozen` | 3604 | The target of `clawback` or `force_revoke_spender` isn't frozen |
+| `InvalidClawbackAmount` | 3605 | The `clawback` amount isn't strictly positive |
+| `InvalidClawbackDestination` | 3606 | The `clawback` destination is the token contract itself |
+
+On a deployment with no configuration, `is_frozen` always returns `false`, so `clawback` and `force_revoke_spender`
+fail with `AccountNotFrozen` (3604), not `NotConfigured`.
+
+See the [compliance specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/compliance.md)
+for the full design rationale.
diff --git a/content/stellar-contracts/tokens/confidential/confidential.mdx b/content/stellar-contracts/tokens/confidential/confidential.mdx
new file mode 100644
index 00000000..e4fa38b6
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/confidential.mdx
@@ -0,0 +1,279 @@
+---
+title: Confidential Token
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential)
+
+The Confidential Token adds private balances and transfers to any SEP-41 token. Users deposit regular tokens
+into a confidential token contract, and from that point on their balances and the amounts they transfer are hidden
+from the public while remaining fully verifiable by the network. Every operation that spends private state carries
+a zero-knowledge proof that the contract verifies on-chain before updating any balance.
+
+The module provides **confidentiality, not anonymity**: sender and recipient addresses stay visible on-chain,
+while amounts and balances do not.
+
+## Overview
+
+The [confidential](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential)
+module wraps an existing SEP-41 token rather than replacing it. Balances are stored as Pedersen commitments on the
+Grumpkin curve, which the contract updates homomorphically (by adding and replacing curve points) without ever
+learning the amounts behind them. Recipients and auditors recover the amounts that concern them by decrypting
+ciphertexts published in the contract's events.
+
+| Hidden | Visible |
+| ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
+| Balances (spendable, receiving, and spender allowances) | Sender and recipient addresses of every operation |
+| Amounts moved by confidential transfers | Deposit and withdrawal amounts, since they cross into or out of the public token |
+| | Which operation was called, and when |
+
+Building confidentiality as a separate contract has several advantages:
+
+- **Works with any SEP-41 token**, including XLM through its Stellar Asset Contract (SAC). No changes are required
+ from the issuer, as long as the token satisfies the [underlying token requirements](#underlying-token-requirements).
+- **Evolves independently** of the token standard: upgrades to the privacy layer never touch the underlying asset.
+- **Is opt-in**: the underlying token keeps working as it does today, and users choose when to move funds into
+ and out of the confidential layer.
+
+## Architecture
+
+### Core Components
+
+A deployment consists of three contracts wired together:
+
+- **Confidential Token**: Implements the `ConfidentialToken` trait. It holds all deposited tokens in a single pooled
+ balance, stores one `ConfidentialAccount` per registered address and one `SpenderDelegation` per owner-spender pair,
+ and calls the verifier for every operation that requires a proof.
+- **Auditor Registry**: Implements the `ConfidentialAuditor` trait and stores auditor public keys indexed by
+ `auditor_id`. A single registry can serve many tokens. See [Auditing](/stellar-contracts/tokens/confidential/auditing).
+- **Verifier Registry**: Implements the `ConfidentialVerifier` trait and stores one UltraHonk verification key per
+ circuit. It can be shared by every token that runs the same protocol version.
+ See [Proof System](/stellar-contracts/tokens/confidential/proof-system).
+
+```mermaid
+graph LR
+ W[Wallet] -->|operation + proof| T[Confidential Token]
+ T -->|verify_proof| V[Verifier Registry]
+ T -->|auditor key lookup| A[Auditor Registry]
+ T <-->|deposit / withdraw| U[Underlying SEP-41 Token]
+ T -.->|events| I[Indexer]
+ I -.->|event history| W
+```
+
+### Off-Chain Components
+
+Because the contract never sees balances or confidential transfer amounts, a large part of the protocol runs on the
+client:
+
+- **Wallet**: Derives keys, generates proofs, decrypts events, and tracks the openings (value and blinding factor)
+ of the account's commitments. See [Wallet Integration](/stellar-contracts/tokens/confidential/wallet-integration).
+- **Indexer**: A durable archive of the contract's events, which wallets and auditors need to recover state.
+ See [Indexing and Recovery](/stellar-contracts/tokens/confidential/indexing).
+- **Circuits**: Six Noir circuits compiled to UltraHonk, shipped in the
+ [`circuits`](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/circuits)
+ directory together with their pinned verification keys.
+
+
+The library ships the on-chain modules (traits and storage helpers), example auditor and verifier registry contracts
+under [`examples/confidential`](https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/confidential),
+and the Noir circuits. The client SDK, the indexer, and the
+[selective disclosure](/stellar-contracts/tokens/confidential/selective-disclosure) layer are specified in the
+[protocol documentation](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/README.md)
+but are not part of the library.
+
+
+## Key Concepts
+
+| Concept | Description |
+| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Commitment** | A balance stored as a single curve point `C = v·G + r·H`. The contract sees only the point; the opening `(v, r)` lives in the owner's wallet. |
+| **Spendable balance** | The part of an account's funds available for transfers and withdrawals. Only operations the owner authorizes modify it, apart from the [compliance extension](/stellar-contracts/tokens/confidential/clawback)'s seizure functions. |
+| **Receiving balance** | A separate accumulator where incoming deposits and transfers land. |
+| **Merge** | An owner-authorized operation that folds the receiving balance into the spendable balance. It requires no proof. |
+| **Zero-knowledge proof** | Generated by the wallet and attached to an operation. It proves the operation is valid (sufficient funds, correct balance updates, honest encryption) without revealing any amount. See [Zero-Knowledge Setting](#zero-knowledge-setting) for the current status. |
+| **Auditor** | A key chosen by each account at registration. Every transfer (direct or by a spender), withdrawal, and spender setup produces ciphertexts that this key can decrypt. |
+| **Spender** | A registered address authorized by an owner to spend from a capped, time-limited allowance. See [Spenders](/stellar-contracts/tokens/confidential/spenders). |
+
+## Installation
+
+The confidential token lives in its own crate, `stellar-confidential`, separate from `stellar-tokens`. The crate is
+not published on crates.io yet, because its UltraHonk verifier backend is itself a git dependency. Add it from the
+repository, pinned to a commit:
+
+```toml
+[dependencies]
+# Use the soroban-sdk version that stellar-contracts uses at the pinned commit.
+soroban-sdk = { version = "28.0.0", features = ["alloc"] }
+stellar-confidential = { git = "https://github.com/OpenZeppelin/stellar-contracts", rev = "" }
+```
+
+The verifier backend links Rust's `alloc` crate, so every contract that depends on `stellar-confidential` (the token
+and both registries) must provide exactly one global allocator. Either enable the `alloc` feature of `soroban-sdk`, as
+above, or register your own `#[global_allocator]`, but not both. Without an allocator, the wasm build fails with
+`no global memory allocator found but one is required`. See
+[Packaging](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/README.md#packaging)
+for the reasons behind this setup.
+
+## Usage
+
+Deploying a confidential token involves three steps:
+
+1. Deploy the verifier registry and register the verification key of each circuit.
+ See [Proof System](/stellar-contracts/tokens/confidential/proof-system).
+2. Deploy an auditor registry, or reuse an existing one, and register at least one auditor key.
+ See [Auditing](/stellar-contracts/tokens/confidential/auditing).
+3. Deploy the confidential token, passing the underlying token, the verifier registry, and the auditor registry
+ to its constructor.
+
+Here's what a basic confidential token contract looks like:
+
+```rust
+use soroban_sdk::{contract, contractimpl, Address, Bytes, Env};
+use stellar_confidential::{
+ storage as confidential, ConfidentialAccount, ConfidentialToken, NoHooks, SpenderDelegation,
+}; // `Bytes`, `ConfidentialAccount` and `SpenderDelegation` are used by the trait's default methods
+
+#[contract]
+pub struct ConfidentialTokenContract;
+
+#[contractimpl]
+impl ConfidentialTokenContract {
+ pub fn __constructor(e: &Env, underlying_asset: Address, verifier: Address, auditor: Address) {
+ // The SEP-41 token being wrapped. Set once; cannot be changed.
+ confidential::set_underlying_asset(e, &underlying_asset);
+ // The verifier and auditor registries.
+ confidential::set_verifier(e, &verifier);
+ confidential::set_auditor(e, &auditor);
+ // Binds every proof to this contract's address. Set once; cannot be changed.
+ confidential::set_address_as_field_element(e);
+ }
+}
+
+#[contractimpl(contracttrait)]
+impl ConfidentialToken for ConfidentialTokenContract {
+ type Hooks = NoHooks;
+}
+```
+
+The `ConfidentialToken` trait provides default implementations for every entry point: `register`, `deposit`,
+`merge`, `withdraw`, `confidential_transfer`, `confidential_transfer_from`, `set_spender`, `revoke_spender`, and the
+read methods. Each state-changing entry point authorizes the caller, decodes the `data: Bytes` payload built by the
+wallet, runs the matching hook, verifies the proof when one is required, and applies the state change.
+See [Operations](/stellar-contracts/tokens/confidential/operations) for what each one does.
+
+### Initialization
+
+The setters in `storage` bypass authorization checks and should only be called from the constructor or from
+admin functions that implement their own access control:
+
+- `set_underlying_asset` and `set_address_as_field_element` are **single-shot**: a second call reverts.
+ The address field is derived from the contract's own address and bound into every account's key derivation,
+ so changing it would invalidate every registered account.
+- `set_verifier` and `set_auditor` can be called again. Rotating either one changes which proofs the contract
+ accepts or which keys auditor ciphertexts are encrypted under, so any rotation path should sit behind a
+ multisig or a timelock. The strongest posture is not to expose one at all. A new auditor registry also
+ re-points every account's `auditor_id`: an account whose id is missing from it can no longer spend. Either
+ rotation also invalidates proofs that are in flight, which wallets then rebuild with a fresh salt.
+
+## Hooks
+
+The `Hooks` associated type is the extension point of the token. Its callbacks run at every state-changing entry point,
+after authorization and payload decoding, and before any state is modified:
+
+- `on_register`, `on_deposit`, `on_merge`, `on_withdraw`
+- `on_transfer`, `on_spender_transfer`
+- `on_set_spender`, `on_revoke_spender`
+
+Callbacks of operations that carry a payload receive the decoded payload (the proof itself is not forwarded).
+Every callback has an empty default body, so an implementation overrides only what it needs, and rejects an operation
+by panicking with an error.
+
+Two implementations are provided:
+
+- `NoHooks`: every callback is a no-op. Use it for deployments without additional controls.
+- `ComplianceHooks`: account freezing, SAC authorization passthrough, and an external authorization policy.
+ See [Compliance](/stellar-contracts/tokens/confidential/compliance).
+
+For pure observability, subscribe to the token's events instead of implementing a hook.
+
+## Underlying Token Requirements
+
+The contract credits and debits confidential balances by the exact amount it passes to the underlying token's
+`transfer`, without re-measuring its own balance. The underlying token must therefore:
+
+- **Move exactly the requested amount**: no fee deducted in transit.
+- **Not rebase**: the contract's balance changes only through transfers the contract itself initiates.
+- **Revert on failure**: a failed `transfer` must revert the whole `deposit` or `withdraw`.
+
+The Stellar Asset Contract and the OpenZeppelin [Fungible Token](/stellar-contracts/tokens/fungible/fungible) meet
+these requirements. Wrapping any other token is the deployer's responsibility.
+
+
+When the underlying token is a SAC, its issuer has two powers over the contract's pooled balance.
+**Freezing or deauthorizing** the contract's address blocks every deposit and withdrawal for all holders until it
+is lifted; confidential transfers between registered accounts keep working. **Clawing back** from the contract's
+address removes value from the pool, leaving it holding less than the sum of all confidential balances: withdrawals
+become first-come-first-served, and whoever exits last cannot fully redeem. See
+[Clawback](/stellar-contracts/tokens/confidential/clawback) for how to seize funds from one account without
+creating a shortfall for everyone else.
+
+
+## Security Considerations
+
+### Zero-Knowledge Setting
+
+
+The on-chain verifier currently implements only the non-zero-knowledge UltraHonk flavor, so proofs are generated
+without the zero-knowledge setting and carry no witness-hiding guarantee: a proof can leak information about its
+private inputs, which include keys, balances, blinding factors, and amounts. Until the verifier supports the
+zero-knowledge flavor, the confidentiality of balances and amounts is not backed by the proof system. The proving
+recipe is provisional and will be finalized together with the verifier. See
+[Generating Proofs](/stellar-contracts/tokens/confidential/proof-system#generating-proofs) for the current status.
+
+
+### Verification Keys
+
+Soundness rests on the verification keys in the verifier registry matching the intended circuits. A wrong key makes
+the contract accept forged proofs, so updating a key should be treated as a break-glass operation.
+See [Proof System](/stellar-contracts/tokens/confidential/proof-system).
+
+### Griefing Resistance
+
+Incoming deposits and transfers only touch the recipient's receiving balance, while spend proofs reference only the
+spendable balance. A third party can therefore not invalidate an owner's in-flight proof by sending funds, and
+cannot trigger a merge on someone else's account since merging requires the owner's authorization.
+
+### Replay Protection
+
+Every spend proof takes the current on-chain commitment as a public input, and a successful operation replaces that
+commitment. A replayed proof references a commitment that no longer exists and fails verification, so no separate
+nonce is needed.
+
+### Key Custody
+
+A viewing key cannot move funds, but it is not a safely shareable read-only credential. Whoever holds it can read
+the account's balances and incoming amounts, and can retroactively open the commitments of the transfers the
+account sent. Auditor keys deserve the same level of custody. To prove a single fact to a third party, use
+[selective disclosure](/stellar-contracts/tokens/confidential/selective-disclosure) instead of sharing a key.
+
+### Confidentiality Limits
+
+Addresses are public, and so are the amounts that enter and leave through `deposit` and `withdraw`. Correlating
+those amounts over time can reveal information that the confidential layer otherwise hides, so integrations
+should not assume that every amount is private.
+
+## TTL Management
+
+Accounts and spender delegations live in persistent storage. Whenever an operation reads one, the library extends
+its TTL back to 30 days once fewer than 29 remain. A new entry starts with the network's minimum persistent TTL
+until an operation first reads it. An entry that is not touched for longer than its TTL is archived and must be
+restored before its next use.
+
+The underlying token, verifier, auditor, and address field are stored in **instance storage**, whose TTL is the
+responsibility of the deployer, as with every other module in this library.
+
+## Specification
+
+This documentation covers what is needed to deploy and integrate the module. The normative
+[protocol specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/README.md)
+covers the cryptographic construction, every circuit constraint, and the companion specifications for compliance,
+the SDK, indexing, and selective disclosure.
diff --git a/content/stellar-contracts/tokens/confidential/indexing.mdx b/content/stellar-contracts/tokens/confidential/indexing.mdx
new file mode 100644
index 00000000..dbebdcad
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/indexing.mdx
@@ -0,0 +1,146 @@
+---
+title: Indexing and Recovery
+---
+
+[Specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/indexer.md)
+
+A confidential balance can only be spent by someone who knows its opening, and the contract never stores openings. A
+wallet that loses its local state rebuilds them from its keys and the account's event history. This page describes
+that recovery procedure and the durable event archive, the *indexer*, it depends on.
+
+
+This page summarizes the indexer specification that accompanies the library. The library does not ship an indexer.
+Deployments that want users to be able to recover from seed need to run, or contract, an archive that meets this
+specification.
+
+
+## Why an Indexer Is Required
+
+Stellar RPC keeps events only for a bounded window, which a node reports through `getHealth` as `oldestLedger` and
+`ledgerRetentionWindow`. Once an account's events fall out of that window, a wallet without local state can still see
+its commitments on-chain but cannot reconstruct the openings needed to spend them. Without a conforming archive,
+wallet-local state should be treated as unrecoverable once events age out of RPC. Wallets with RPC-only access must
+therefore sync at least once per retention window and warn when they have not: a crediting event, or a
+`RevokeSpender` after the latest checkpoint, that ages out before the wallet applies it is lost for good.
+
+RPC and the archive work together. A client can read the recent tail from RPC and older history from the archive,
+stitching the two at a *seam*. The seam sits strictly above the RPC retention floor, with a margin, because the floor
+advances while the client is querying. The two legs cover disjoint ledger ranges, and the client still deduplicates
+by event id at the boundary. The archive does not have to keep up with the chain head, but it must never fall behind
+the seam. If a configured archive fails,
+the whole sync fails: silently falling back to RPC alone and saving the resulting sync position would skip the
+archived range forever.
+
+## How Recovery Works
+
+Recovery uses two anchors in the account's history:
+
+```mermaid
+graph LR
+ R[Register] -.-> T0["T₀: last Merge or Clawback
at or before the checkpoint"]
+ T0 --> CP["Latest checkpoint
Withdraw, outgoing Transfer, or SetSpender"]
+ CP --> NOW[Now]
+```
+
+1. **Spendable side: the latest checkpoint.** Every withdrawal, outgoing transfer, and `set_spender` emits the owner's
+ new spendable balance, encrypted under the viewing key, together with the salt that determines the new blinding.
+ The latest such event recovers the spendable opening in a single lookup, without any earlier history. An account
+ with no checkpoint since `Register` starts its spendable opening at zero.
+2. **Receiving side: replay from `T₀`.** The receiving balance has no checkpoint. The wallet finds `T₀`, the most
+ recent merge or clawback at or before the checkpoint (or `Register` if there is none), starts the receiving
+ opening at zero, and replays every later event in order:
+ - Deposits and incoming transfers credit the receiving side, including the incoming side of a self-transfer.
+ - Checkpoint events leave the spendable side alone, since step 1 already captured it.
+ - A `RevokeSpender` at or before the checkpoint is skipped, since the checkpoint already absorbed it. One after
+ the checkpoint folds into the spendable side.
+ - A merge after the checkpoint folds as usual, and a clawback folds the same way and then subtracts its public
+ `amount` from the spendable side.
+3. **Verify.** The wallet re-commits both openings and compares them against the on-chain commitments.
+
+The replay window is set by how long the account went without merging before its latest checkpoint, not by how often
+it spends. Wallets that merge regularly keep it short; in the worst case, funds received but never merged, it reaches
+back to registration. See
+[Recovery](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/wallet-state.md#recovery)
+for the exact procedure.
+
+Recovery needs only the viewing key, the event history, and the on-chain commitments, and nothing from the auditor,
+counterparties, or the contract admin. Because commitments are binding, a tampered or truncated history cannot
+produce an opening that verifies; it produces a detectable mismatch. Incoming-transfer spam, even zero-value, costs
+the recipient indexer storage and replay time but never affects correctness.
+
+## What the Indexer Stores
+
+For every in-scope event, the archive keeps the contract id, ledger sequence and close time, transaction hash, the
+transaction's position within its ledger, the event index, and the event's topics and data. Verbatim XDR is the
+recommended payload format: it is the on-chain wire form and survives renames in the Rust bindings. A decoded form is
+also acceptable if it reproduces the on-chain event exactly.
+
+**Events in scope.** These events play a role in recovery:
+
+- `Register` starts an account's history and bounds the worst-case replay.
+- `Deposit` and the recipient side of `Transfer` and `SpenderTransfer` are replayed into the receiving opening.
+- `Merge` and, with the compliance extension, `Clawback` are the receiving-side anchors.
+- `Withdraw`, the sender side of `Transfer`, and `SetSpender` are spendable-side checkpoints. Spenders recover their
+ allowance from the on-chain delegation entry, not from the archive.
+- `RevokeSpender` folds the reclaimed allowance back into the spendable opening. The compliance extension's forced
+ revocation emits the same event.
+
+Configuration and compliance events are not needed for balance recovery, but are worth archiving for forensics.
+
+**Account attribution.** An event belongs to every account address in its topics (a `Transfer` to both sender and
+recipient, a `SpenderTransfer` to owner, recipient, and spender), never to the transaction's source account. The
+indexer can filter per account on the server, which is recommended for busy contracts, or serve the whole contract
+stream for clients to filter.
+
+**Ordering.** Replay is order-sensitive, so the archive preserves the total order
+`(ledger_seq, tx_application_order, event_index)`. The event id `(ledger_seq, tx_hash, event_index)` identifies an
+event but does not order it within a ledger.
+
+## Ingestion and Retention
+
+- **Source.** Any source that yields the complete, final event stream works: Stellar RPC `getEvents`, Horizon, or a
+ captive core. Stellar ledgers are final at close, so there are no reorgs to handle.
+- **Freshness and gaps.** The archive tracks which ledger ranges it has ingested and backfills gaps while the source
+ still has them. A gap that can no longer be filled is permanent and must be reported as incomplete.
+- **Idempotency.** Ingestion is at-least-once, deduplicated by event id.
+- **Retention.** The full per-account history is kept indefinitely, because no pruning horizon is safe. A dormant
+ account's latest checkpoint can be arbitrarily old, an account that never merges replays from registration, and a
+ `RevokeSpender` after the latest checkpoint is the only surviving record of the allowance it folded, since the
+ delegation entry is deleted in the same call.
+
+## API Capabilities
+
+The specification defines capabilities rather than a transport, with a recommended REST shape in
+[API Surface](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/indexer.md#api-surface):
+
+- **Ordered history** (required): all in-scope events for an account within a ledger range, in total order,
+ paginated, each with its event id and payload.
+- **Completeness signal** (required): every response states whether the indexer holds a gap-free history for the
+ whole requested range.
+- **Ingestion status** (required): the latest fully ingested ledger, so clients can bound staleness and check it
+ against their seam.
+- **Latest checkpoint** (recommended): the most recent checkpoint for an account at or before a given ledger, so a
+ dormant account need not download the spend history before it. It does not return `T₀`, so the client still reads
+ the ordered history from `T₀` onward, which for an account that never merged is its whole history.
+
+The specification is versioned with the protocol documentation; breaking changes to the archived record or the
+required capabilities bump that version.
+
+## Trust Model
+
+The indexer is trusted only for availability and completeness, never for confidentiality or integrity:
+
+- **Confidentiality.** Everything it holds is public chain data, so a curious indexer learns nothing a chain observer
+ does not.
+- **Integrity.** Wallets verify reconstructed openings against the on-chain commitments, so tampering is detected.
+- **Withholding.** A malicious or broken indexer can deny recovery. RPC independently serves the recent window, and
+ for older history, wallets should support several independent archive endpoints, with deployments running or
+ contracting at least two. Clients must pass the completeness signal on to their callers, because an incomplete
+ range and a tampered one both end in the same failed consistency check.
+
+### Auditor Recovery
+
+Auditors depend on the archive even more than holders do. The blinding of each allowance commitment is escrowed only
+in the `SetSpender` or `SpenderTransfer` event that wrote it, never in contract storage, so a missed or unarchived
+event leaves the auditor without that opening. An auditor client should check each reconstructed allowance opening
+against the stored allowance commitment. See [Auditing](/stellar-contracts/tokens/confidential/auditing).
diff --git a/content/stellar-contracts/tokens/confidential/operations.mdx b/content/stellar-contracts/tokens/confidential/operations.mdx
new file mode 100644
index 00000000..369ae41a
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/operations.mdx
@@ -0,0 +1,255 @@
+---
+title: Operations
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential)
+
+The `ConfidentialToken` trait exposes eight state-changing entry points and three read methods. Every
+state-changing entry point authorizes its caller with `require_auth()`. Operations that consume private state also
+carry a zero-knowledge proof, which the token checks through the
+[verifier registry](/stellar-contracts/tokens/confidential/proof-system) before changing any storage.
+
+This page covers the owner's lifecycle: registering, moving value in and out, and transferring.
+Delegated spending has its own page: [Spenders](/stellar-contracts/tokens/confidential/spenders).
+
+## Entry Points
+
+| Function | Authorized by | Proof | Effect |
+| --- | --- | --- | --- |
+| `register(account, auditor_id, data)` | `account` | Yes | Creates the confidential account, with both balances at zero |
+| `deposit(from, to, amount)` | `from` | No | Pulls `amount` of the underlying token into the contract and credits `to`'s receiving balance |
+| `merge(account)` | `account` | No | Moves the receiving balance into the spendable balance |
+| `confidential_transfer(from, to, data)` | `from` | Yes | Debits `from`'s spendable balance and credits `to`'s receiving balance with a hidden amount |
+| `withdraw(from, to, amount, data)` | `from` | Yes | Debits `from`'s spendable balance and sends `amount` of the underlying token to `to` |
+| `set_spender(account, spender, live_until_ledger, data)` | `account` | Yes | Locks part of the spendable balance into an allowance for `spender` |
+| `confidential_transfer_from(spender, from, to, data)` | `spender` | Yes | Spends from the allowance `from` granted to `spender` |
+| `revoke_spender(account, spender)` | `account` | No | Returns the remaining allowance to the spendable balance |
+
+Each entry point checks authorization first, then decodes `data`, then runs the deployment's
+[`Hooks`](/stellar-contracts/tokens/confidential/confidential#hooks) callback, then hands off to the storage layer. The
+storage layer verifies the proof, updates state and emits the event.
+
+## Spendable and Receiving Balances
+
+Every account holds two balances, each stored as a Pedersen commitment `C = v·G + r·H` that hides the
+value `v` behind a blinding factor `r`:
+
+- **Spendable balance**: what the owner can send, withdraw, or delegate. Only operations the owner
+ authorizes can change it, except in deployments with the compliance extension, where the admin's
+ [`clawback`](/stellar-contracts/tokens/confidential/clawback) and `force_revoke_spender` can too.
+- **Receiving balance**: where deposits and incoming transfers land. The contract adds to it
+ homomorphically, with no proof or action from the recipient.
+
+The split prevents griefing. A spend proof references the spendable commitment as it is on-chain when
+the proof is built. Incoming transfers touch only the receiving commitment, so no other user can
+change the commitment an in-flight proof references. An account can also receive any number of transfers
+without the owner having to do anything. See
+[Griefing Resistance](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/security.md#griefing-resistance).
+
+A proof is also tied to the auditor keys and the verification key in force when it is built. If the
+deployment rotates an [auditor key](/stellar-contracts/tokens/confidential/auditing#key-rotation) or the
+[verifier](/stellar-contracts/tokens/confidential/proof-system#updating-a-verification-key) while a proof
+is in flight, the call reverts with `InvalidProof` and no state change. The wallet rebuilds the proof with a
+fresh salt and resubmits.
+
+The cost is that received funds must be merged before they can be spent.
+
+## Register
+
+An account must register before it can receive deposits or transfers. The wallet derives a spending
+key and a viewing key from a single secret (see [Wallet Integration](/stellar-contracts/tokens/confidential/wallet-integration))
+and proves the two public keys are well formed and tied to this token contract.
+
+The caller also picks an `auditor_id`. It must exist in the auditor registry; an unknown id reverts with
+the registry's `AuditorNotRegistered`. Once set it cannot change,
+and it decides which auditor receives the encrypted copies of this account's activity (see
+[Auditing](/stellar-contracts/tokens/confidential/auditing)). The core does not restrict which auditor an account
+picks; deployments that need to restrict it do so in `Hooks::on_register`.
+
+Registration is single-use: a second call for the same address reverts with `AccountAlreadyRegistered`.
+The contract checks this after the auditor lookup and proof verification, so a retry with an invalid
+proof fails with `InvalidProof` first. The proof is also bound to the registering address, so published
+registration data cannot be replayed to create a duplicate-key account under a different address.
+
+## Deposit
+
+```rust
+// `from` authorizes the underlying SEP-41 transfer; `to` must be registered.
+token_client.deposit(&depositor, &recipient, &500);
+```
+
+The contract pulls `amount` of the underlying token from `from` and adds a zero-blinding commitment
+`amount·G` to `to`'s receiving balance. No proof is needed because the amount is already public. The
+depositor doesn't need a confidential account; only the recipient does. Negative amounts revert with
+`NegativeAmount`.
+
+## Merge
+
+```rust
+token_client.merge(&owner);
+```
+
+Merge adds the receiving commitment to the spendable commitment and resets the receiving balance to
+zero. It needs no proof: adding two commitments yields a commitment to the sum of their values and
+blinding factors, so no value is created or lost. Only the owner can authorize it.
+
+The owner's wallet knows the new opening because it has tracked every deposit and transfer credited to
+the receiving balance. Wallets should prompt a merge before any spend so that received funds are
+available.
+
+## Confidential Transfer
+
+```rust
+// `transfer_data` is the XDR-encoded `TransferData` built by the sender's wallet.
+token_client.confidential_transfer(&sender, &recipient, &transfer_data);
+```
+
+The sender's wallet proves that the sender holds at least the transfer amount, that the new spendable
+commitment is the old balance minus the amount, and that the amount is encrypted correctly for the
+recipient and both auditors. The contract checks the proof, replaces the sender's spendable
+commitment, and adds the transfer commitment to the recipient's receiving balance.
+
+```mermaid
+sequenceDiagram
+ participant S as Sender wallet
+ participant T as Confidential Token
+ participant A as Auditor registry
+ participant V as Verifier registry
+ participant R as Recipient wallet
+
+ S->>T: confidential_balance(sender), confidential_balance(recipient)
+ S->>A: get_key(sender auditor_id), get_key(recipient auditor_id)
+ S->>S: Build proof, commitments and ciphertexts
+ S->>T: confidential_transfer(from, to, data)
+ T->>A: get_key(...) for both auditor ids
+ T->>V: verify_proof(Transfer, public_inputs, proof)
+ V-->>T: true
+ T->>T: Replace sender spendable, add to recipient receiving
+ T-->>R: Transfer event
+ R->>R: Decrypt amount, update receiving balance
+```
+
+The recipient's wallet watches for `Transfer` events where it is `to`, and also for `SpenderTransfer`
+events, which credit it the same way (see
+[Spenders](/stellar-contracts/tokens/confidential/spenders#spending-from-an-allowance)). It derives a
+shared secret from the event's ephemeral public key and its own viewing key. With that secret and the
+event's salt, it decrypts the amount and derives the blinding factor. The recipient does nothing on-chain.
+
+Observers see that `from` transferred to `to`. They do not see the amount or either balance.
+
+## Withdraw
+
+```rust
+// `withdraw_data` is the XDR-encoded `WithdrawData` built by the owner's wallet.
+token_client.withdraw(&owner, &destination, &200, &withdraw_data);
+```
+
+The wallet proves that the spendable balance covers `amount` and that the new commitment is correct.
+The contract replaces the spendable commitment and sends `amount` of the underlying token to `to`, which
+can be any address. The withdrawn amount is public; the remaining balance stays hidden.
+
+## Payload Encoding
+
+Proof-carrying operations take a single `data: Bytes` argument: the XDR encoding of a `…Data` struct
+(`RegisterData`, `WithdrawData`, `TransferData`, `SetSpenderData`, `SpenderTransferData`). Each struct
+contains the operation's `payload` and the `proof` bytes. The payload holds the values the prover
+produced: the two public keys for `register`, and the new commitments, ephemeral key, salt and
+ciphertexts for the other operations. All of these come from the wallet. The contract loads everything
+else itself, from its own storage, the auditor registry, or the call's arguments (such as `amount`).
+
+```rust
+use soroban_sdk::xdr::ToXdr;
+use stellar_confidential::{TransferData, TransferPayload};
+
+// `payload` and `proof` are produced by the wallet's prover.
+let data = TransferData { payload, proof }.to_xdr(&e);
+token_client.confidential_transfer(&sender, &recipient, &data);
+```
+
+The structs are `#[contracttype]` values and Soroban's XDR encoding is canonical, so any client
+compiled against the same type definitions produces identical bytes. A payload that does not decode
+reverts with `InvalidData`. A field or point coordinate that is not the canonical encoding of its field
+element reverts with `NonCanonicalEncoding` (see
+[Proof System](/stellar-contracts/tokens/confidential/proof-system)).
+
+## Authorization Model
+
+Authorization and proofs do different jobs, and every proof-carrying operation requires both.
+`require_auth()` proves that the named address approved this exact invocation, arguments included. The
+proof shows that whoever built it knows secrets consistent with the on-chain state the contract
+supplies. Because of this, a proof is useless to anyone who can't also authorize as the account, and an
+authorization is useless without a valid proof.
+
+In the core, `confidential_transfer_from` is the one case where the funds' owner does not sign: the owner
+approved the spender ahead of time at `set_spender`. Deployments with the compliance extension add two
+admin-authorized exceptions, `clawback` and `force_revoke_spender` (see
+[Clawback](/stellar-contracts/tokens/confidential/clawback)).
+
+Proofs cannot be replayed. Each spending proof takes the current spendable (or allowance) commitment
+as an input, and a successful call replaces that commitment. A replayed proof therefore points at a
+commitment that no longer exists and fails verification. No nonce or nullifier is needed.
+
+## Events
+
+Events are how wallets, auditors and indexers learn what happened, because storage holds only
+commitments.
+
+- `Register`, `Deposit` and `Merge` carry only addresses, the `auditor_id`, and the public deposit
+ amount.
+- `Transfer` carries the ephemeral public key, the salt, the amount encrypted for the recipient, the
+ sender's encrypted new balance (for the sender's own recovery), and the ciphertexts for the
+ recipient's and the sender's auditors.
+- `Withdraw` carries the public amount, the ephemeral key, the salt, the encrypted new balance, and the
+ sender-auditor ciphertexts.
+- `SetSpender`, `SpenderTransfer` and `RevokeSpender` are covered on the
+ [Spenders](/stellar-contracts/tokens/confidential/spenders#events) page.
+
+A wallet that loses its local state rebuilds both balances from these events. It restores the spendable
+balance from its latest checkpoint (a `Withdraw`, a `Transfer` it sent, or a `SetSpender`), then replays
+the events that follow. Stellar RPC
+keeps events for a limited window only, so recovery depends on a durable archive (see
+[Indexing and Recovery](/stellar-contracts/tokens/confidential/indexing)).
+
+## Read Methods
+
+```rust
+let account = token_client.confidential_balance(&owner);
+let active = token_client.is_spender(&owner, &spender);
+let delegation = token_client.get_spender_delegation(&owner, &spender);
+```
+
+`confidential_balance` returns the `ConfidentialAccount` stored for an address. It reverts if the
+address isn't registered.
+
+| Field | Description |
+| --- | --- |
+| `spending_public_key` | Grumpkin point authorizing spends; set at registration |
+| `viewing_public_key` | Grumpkin point senders use to encrypt for this account; set at registration |
+| `spendable_commitment` | Commitment to the spendable balance |
+| `receiving_commitment` | Commitment to the receiving balance |
+| `auditor_id` | Auditor key index fixed at registration |
+
+The balances are commitments, not numbers. Only a wallet holding the account's openings can turn them
+into amounts. Wallets read this struct to bootstrap; indexers read it to check that the state they
+replayed matches the chain.
+
+`is_spender` and `get_spender_delegation` are described on the
+[Spenders](/stellar-contracts/tokens/confidential/spenders) page.
+
+## Failed Transactions
+
+Every proof-carrying operation except `register` carries a salt: `sigma`, or `sigma_a_new` for a spender
+transfer. The salt determines the new commitment's blinding factor and every encryption mask. The wallet
+must sample a fresh salt for every attempt, so when a transaction reverts, it picks a new salt and
+rebuilds the proof. Reusing the old salt would let an observer link the failed attempt to the retry,
+and a retry with a different amount would leak the difference between the two amounts. The contract
+cannot enforce freshness; it is the wallet's job (see
+[Salts and Retries](/stellar-contracts/tokens/confidential/wallet-integration#salts-and-retries)). The salt is
+published in the event, so the owner and the auditor can still reconstruct the randomness. See
+[Revert Safety](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/security.md#revert-safety).
+
+## Further Reading
+
+The full constraint set, public inputs and post-verification state for each operation are specified in
+[Operations](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/operations/README.md)
+and [Interface](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/interface.md).
diff --git a/content/stellar-contracts/tokens/confidential/proof-system.mdx b/content/stellar-contracts/tokens/confidential/proof-system.mdx
new file mode 100644
index 00000000..0b420ac0
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/proof-system.mdx
@@ -0,0 +1,302 @@
+---
+title: Proof System
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/src/verifier)
+
+Every confidential operation that consumes private state comes with a proof that the state change is
+valid. The proofs are meant to be zero-knowledge, but the current verifier does not support that yet;
+see [Generating Proofs](#generating-proofs). The circuits are written in [Noir](https://noir-lang.org/)
+and compiled to UltraHonk. Proofs are generated client-side by the wallet. The token verifies each one
+with a cross-contract call to a separate **verifier registry**, which stores one verification key (VK)
+per circuit and runs the UltraHonk verifier from
+[NethermindEth/rs-soroban-ultrahonk](https://github.com/NethermindEth/rs-soroban-ultrahonk).
+
+The library ships the circuit sources, the pinned verification keys, and the verifier registry module.
+It does not ship a prover or client SDK; see [Wallet Integration](/stellar-contracts/tokens/confidential/wallet-integration).
+
+## Circuits
+
+| Circuit | `CircuitType` | Entry point | What it proves |
+| --- | --- | --- | --- |
+| Register | `Register = 0` | `register` | The spending and viewing keys are well formed, derive from one secret, and are bound to this token contract |
+| Withdraw | `Withdraw = 1` | `withdraw` | The spendable balance covers the public amount; the new commitment is correct; the sender's auditor receives the new balance |
+| Transfer | `Transfer = 2` | `confidential_transfer` | The balance covers the hidden amount; amounts are in range; the amount is encrypted correctly for the recipient and for both auditors |
+| Spender Transfer | `SpenderTransfer = 3` | `confidential_transfer_from` | The allowance covers the amount; the new allowance commitment is correct; recipient and auditor encryptions are correct |
+| Set Spender | `SetSpender = 4` | `set_spender` | The allowance is carved out of the spendable balance; the delegation viewing key is derived and escrowed to the spender correctly |
+| Clawback | `Clawback = 100` | `clawback` (compliance extension) | The seized amount does not exceed the target's total holdings; see [Clawback](/stellar-contracts/tokens/confidential/clawback) |
+
+`deposit`, `merge` and `revoke_spender` have no circuit. They only combine commitments, which the
+contract does directly with Grumpkin point arithmetic. `deposit` first forms the commitment
+`amount·G`, with zero blinding, on-chain.
+
+The `CircuitType` values are part of the on-chain interface and are never renumbered. Core circuits use
+`0..=4`; circuits shipped by optional extensions start at `100`. A deployment that does not enable
+clawback does not need the `Clawback` key.
+
+Elliptic-curve scalar multiplication dominates proving cost. Counted as `multi_scalar_mul` calls,
+Register uses 2, Withdraw 5, Set Spender 7, Transfer and Spender Transfer 8 each, and Clawback 3. The
+design targets proof generation in single-digit seconds on contemporary hardware. See
+[Circuit Cost Analysis](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/proof-system.md#circuit-cost-analysis).
+
+## Verification Flow
+
+For every proof-carrying operation, the token contract:
+
+1. Loads the trusted inputs from its own state: the account's current commitments and public keys, the
+ relevant auditor keys from the auditor registry, the delegation entry for spender operations, and
+ its own contract-address field.
+2. Combines them with values derived from the invocation arguments, such as the withdrawn amount or
+ the registering address, and with the values the prover supplied in the `data` payload. The order
+ is fixed per circuit. Each field element and point coordinate is checked for canonical encoding.
+3. Calls `verify_proof(circuit_type, public_inputs, proof)` on the verifier registry, and reverts with
+ `InvalidProof` if it returns `false`. A missing or unparsable key is not a `false`: the registry
+ panics with `VerificationKeyNotRegistered` (`#3401`) or `InvalidVerificationKey` (`#3403`), and
+ that error aborts the token call.
+4. Applies the state change and emits the event.
+
+The contract builds the public inputs itself rather than accepting them from the caller. The verifier
+sees only field elements and can't tell which account or contract they describe. If the contract
+accepted, say, the sender's public key from the payload, a perfectly sound proof could verify against
+the wrong account. Only invocation arguments, which `require_auth()` covers, and values the circuit
+constrains may come from the caller. See
+[Public Input Sources](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/operations/README.md#public-input-sources).
+
+The canonical-encoding check covers a quirk of the Soroban host. The host silently reduces a 32-byte
+value `≥ r` modulo the field order instead of rejecting it, so two different byte strings could map to
+the same field element. The contract rejects non-canonical values with `NonCanonicalEncoding` before
+verification. That way every value it stores or emits has exactly one byte representation.
+
+Proofs are also bound to their context. Owner and clawback proofs commit to a field derived from the
+token's contract address, which is computed once at construction. Spender-transfer proofs are bound
+indirectly, through the allowance commitment that a set-spender proof created on that token. A proof
+for one token deployment therefore never verifies on another, even when both share the same verifier
+registry. Registration proofs are additionally bound to the registering address. Clawback proofs are
+bound to the target account, the settlement destination and a per-account nonce.
+
+## Verifier Registry
+
+The verifier registry is a separate contract so that VK management is scoped to its own admin,
+distinct from the token's, and so that keys can be managed without redeploying the token. A single
+registry can serve every confidential token on the same protocol version, because per-token binding
+happens inside the circuits, not in the verifier.
+
+The `ConfidentialVerifier` trait provides `verify_proof` and `get_verification_key` as defaults.
+Registering and updating keys are privileged, so the implementor writes those two methods with its own
+access control:
+
+```rust
+// `Vec` is used by the AccessControl trait's default methods.
+use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, Env, Symbol, Vec};
+use stellar_access::access_control::{self as access_control, AccessControl};
+use stellar_macros::only_role;
+use stellar_confidential::verifier::{
+ storage as verifier, CircuitType, ConfidentialVerifier,
+};
+
+const MANAGER_ROLE: Symbol = symbol_short!("manager");
+
+#[contract]
+pub struct ConfidentialVerifierContract;
+
+#[contractimpl]
+impl ConfidentialVerifierContract {
+ pub fn __constructor(e: &Env, admin: Address, manager: Address) {
+ access_control::set_admin(e, &admin);
+ access_control::grant_role_no_auth(e, &manager, &MANAGER_ROLE, &admin);
+ }
+}
+
+#[contractimpl(contracttrait)]
+impl ConfidentialVerifier for ConfidentialVerifierContract {
+ #[only_role(operator, "manager")]
+ fn register_verification_key(
+ e: &Env,
+ circuit_type: CircuitType,
+ verification_key: Bytes,
+ operator: Address,
+ ) {
+ verifier::register_verification_key(e, circuit_type, &verification_key);
+ }
+
+ #[only_role(operator, "manager")]
+ fn update_verification_key(
+ e: &Env,
+ circuit_type: CircuitType,
+ new_verification_key: Bytes,
+ operator: Address,
+ ) {
+ verifier::update_verification_key(e, circuit_type, &new_verification_key);
+ }
+}
+
+#[contractimpl(contracttrait)]
+impl AccessControl for ConfidentialVerifierContract {}
+```
+
+Like every contract that depends on `stellar-confidential`, the registry needs the crate as a git
+dependency and a global allocator. See [Installation](/stellar-contracts/tokens/confidential/confidential#installation).
+
+This example gates key updates behind the same `manager` role as registration, purely for
+illustration. See [Updating a Verification Key](#updating-a-verification-key) for what a production
+deployment should do instead.
+
+Verification keys live in **instance** storage. As with every module in this library, extending the
+instance TTL is the contract developer's responsibility.
+
+## Registering Verification Keys
+
+The pinned keys are committed under
+[`circuits/vks/`](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/circuits/vks),
+two files per circuit:
+
+- `.vk.bin`: the 1760-byte packed binary key the on-chain verifier parses. **Register this one.**
+- `.vk.json`: a JSON array of field elements, for review only. The verifier can't parse it.
+
+Register one key per `CircuitType` before the token goes live. Registration fails with
+`VerificationKeyAlreadyRegistered` if that circuit already has a key.
+
+The registry stores the bytes without inspecting them. A wrong file, such as a `.vk.json`, registers
+without error and fails only once `verify_proof` runs, with `InvalidVerificationKey`. Registration is
+single-shot per circuit, so fixing it then takes `update_verification_key`. After registering, call
+`verify_proof` once per circuit with junk bytes. It returns `false` if the key parses and panics with
+`InvalidVerificationKey` if it doesn't.
+
+The test below, modeled on the example contract's tests, registers the Transfer key with the registry
+above and runs that check:
+
+```rust
+use soroban_sdk::{testutils::Address as _, Address, Bytes, Env};
+use stellar_confidential::verifier::CircuitType;
+
+use crate::contract::{ConfidentialVerifierContract, ConfidentialVerifierContractClient};
+
+const TRANSFER_VK: &[u8; 1760] = include_bytes!("vks/transfer.vk.bin");
+
+#[test]
+fn register_transfer_key() {
+ let e = Env::default();
+ e.mock_all_auths();
+ let admin = Address::generate(&e);
+ let manager = Address::generate(&e);
+ let verifier_id = e.register(ConfidentialVerifierContract, (&admin, &manager));
+ let client = ConfidentialVerifierContractClient::new(&e, &verifier_id);
+
+ client.register_verification_key(
+ &CircuitType::Transfer,
+ &Bytes::from_array(&e, TRANSFER_VK),
+ &manager,
+ );
+
+ // The key parses: a junk proof returns `false` instead of panicking with
+ // `InvalidVerificationKey`.
+ let junk = Bytes::from_array(&e, &[0u8; 32]);
+ assert!(!client.verify_proof(&CircuitType::Transfer, &junk, &junk));
+}
+```
+
+On a live network, the manager signs the same `register_verification_key` call, passing the contents
+of the `.vk.bin` file as `verification_key`.
+
+| `CircuitType` | Key file |
+| --- | --- |
+| `Register` | `register.vk.bin` |
+| `Withdraw` | `withdraw.vk.bin` |
+| `Transfer` | `transfer.vk.bin` |
+| `SpenderTransfer` | `spender_transfer.vk.bin` |
+| `SetSpender` | `set_spender.vk.bin` |
+| `Clawback` | `clawback.vk.bin` (only with the clawback extension) |
+
+The keys can be reproduced from the circuit sources with the pinned toolchain (`nargo 1.0.0-beta.11`,
+`bb 0.87.0`), and CI fails if a regenerated key differs from the committed one. Publish the circuit
+source, the toolchain versions and the key files with every deployment, so that users can check the
+keys themselves.
+
+## Updating a Verification Key
+
+
+Updating a verification key is soundness-critical. A wrong, corrupted or malicious key makes its
+circuit accept forged proofs: minting value, draining accounts, or impersonating users. The registry
+stores keys as opaque bytes and cannot detect a bad one. Activating a new key also makes every
+in-flight proof for that circuit fail, so wallets must regenerate and resubmit.
+
+
+The strongest posture is full immutability: fix the token's dependencies and every verification key at
+deployment, and require a fresh deployment for any circuit change. If a deployment exposes
+`update_verification_key` at all, keep it for fixing a discovered soundness bug. Put it behind strong
+governance, at minimum a multisig plus a [timelock](/stellar-contracts/governance/timelock-controller).
+Publish enough material for anyone to reproduce the new key before it activates.
+
+The trait has no default for `update_verification_key`, so every implementation exposes it as an
+entry point. For an immutable registry, pass every key to `__constructor` and register it there with
+`verifier::register_verification_key`. Then make `update_verification_key` always panic, and
+`register_verification_key` too if no circuit is left to add.
+
+The same applies to the token's link to its verifier. `set_underlying_asset` and the contract-address
+field can only be set once, but `set_verifier` and `set_auditor` can be called again. If your token
+contract exposes a path that calls `set_verifier` after construction, that path is as
+soundness-critical as a key update. Monitor the registry's `verification_key_updated` event and the
+token's `verifier_set` event. See
+[Governance and Upgradeability](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/system-model.md#governance-and-upgradeability).
+
+## Generating Proofs
+
+Proving happens in the wallet. Proofs the deployed verifier accepts must be generated with the same
+pinned toolchain and two non-default settings:
+
+```bash
+nargo execute --package circuit_
+bb prove -s ultra_honk --oracle_hash keccak \
+ -b target/circuit_.json -w target/.gz -o
+```
+
+- `--oracle_hash keccak` is required. The on-chain verifier reproduces the Fiat-Shamir transcript with
+ Keccak, while `bb` defaults to Poseidon2.
+- `--zk` must not be passed.
+
+
+The on-chain verifier currently implements only the non-zero-knowledge UltraHonk flavor, so proofs are
+generated without `--zk`. Until the verifier supports the zero-knowledge flavor, proofs have no
+witness-hiding guarantee. They carry data that depends on the private inputs (spending key, balances,
+blinding factors, amounts) and are not guaranteed to hide them. The
+[verifier's README](https://github.com/NethermindEth/rs-soroban-ultrahonk/blob/097da17df8b0b971d71885566034958a8caebab4/crates/ultrahonk-soroban-verifier/README.md)
+at the pinned revision warns against using it for privacy-sensitive circuits. Upstream marks this
+proving recipe as provisional: it will be finalized together with the verifier, including the
+zero-knowledge setting. Check the
+[verification-key README](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/circuits/vks/README.md)
+for the current status.
+
+
+## Trusted Setup
+
+UltraHonk relies on a structured reference string (SRS) produced by a setup ceremony. Anyone who knew
+the ceremony's secret could forge proofs for any statement. The Noir/Barretenberg toolchain uses the
+**Aztec Ignition** SRS. That ceremony had 176 independent participants and is secure as long as at
+least one of them destroyed their contribution. Its transcript and verification code are public.
+
+The verifier contract does not store the full SRS. It embeds only the two G2 points its final pairing
+check uses. The verification keys are derived from the SRS offline, and anyone with the circuit source
+and the public transcript can check them. See
+[Structured Reference String](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/proof-system.md#structured-reference-string).
+
+## Network Requirements
+
+UltraHonk verification and the on-chain Grumpkin arithmetic depend on the BN254 host functions
+introduced by [CAP-80](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0080.md). The
+rollout spans two protocol versions, and both are needed:
+
+- **Protocol 25:** `bn254_g1_add`, `bn254_g1_mul`, `bn254_multi_pairing_check`.
+- **Protocol 26:** `bn254_g1_msm`, `bn254_g1_is_on_curve`, and the `bn254_fr_*` scalar-field
+ arithmetic that underlies Grumpkin point operations.
+
+The token also hashes addresses with the `poseidon2_permutation` host function, available since
+protocol 25.
+
+The host functions alone require **protocol 26**. In practice the minimum is higher: the library pins
+`soroban-sdk` 28, and a contract built with it can only be deployed and run on a network at
+**protocol 28** or later.
+
+## Further Reading
+
+- [Proof System](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/proof-system.md): circuit summary, Noir primitives, on-chain point arithmetic
+- [Circuit sources](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/circuits)
diff --git a/content/stellar-contracts/tokens/confidential/selective-disclosure.mdx b/content/stellar-contracts/tokens/confidential/selective-disclosure.mdx
new file mode 100644
index 00000000..22028d39
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/selective-disclosure.mdx
@@ -0,0 +1,151 @@
+---
+title: Selective Disclosure
+---
+
+[Specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/selective-disclosure/README.md)
+
+The confidential token hides amounts by default, but regulated finance routinely asks an account holder to prove one
+specific fact to one specific party. A bank's compliance desk asks to confirm the amount of an incoming payment, a tax
+authority asks for the total received over a quarter, and a KYC provider asks for evidence that a balance is below a
+threshold. Selective disclosure answers these requests with a zero-knowledge proof bound to a single on-chain event (or
+the current balance) and to a single recipient, without handing over a viewing key.
+
+
+Selective disclosure is an off-chain layer specified alongside the library. It needs no changes to the token contract,
+but the library does not ship the disclosure circuits: the circuits in `circuits/` today cover only the core
+operations. Disclosure proofs are verified off-chain by the recipient, not by the token's verifier registry.
+
+
+## The Selective-Disclosure Gap
+
+[Auditing](/stellar-contracts/tokens/confidential/auditing) gives each auditor a key that decrypts every transfer of
+the accounts in its scope. That suits regulator access, but it is the wrong tool for routine requests:
+
+- **Granularity.** An auditor key cannot decrypt one transfer without being able to decrypt all of them.
+- **Counterparties.** Many requests come from parties that are not the auditor, and routing all of them through the
+ auditor concentrates trust and adds latency.
+- **Recipient binding.** A plaintext answer can be re-shared or replayed, with nothing tying it to the party that
+ asked.
+
+Handing over the viewing key is not an alternative, since it exposes the account's entire history (see
+[Wallet Integration](/stellar-contracts/tokens/confidential/wallet-integration)).
+
+## Design Goals
+
+Each proof covers one named on-chain event, a finite set of events, or the current balance, never an account's
+history. It is bound to the recipient's public key and a fresh nonce, so it cannot answer a different request and its
+value opens only for that recipient. The recipient checks it against the event log, account records, and auditor
+registry without trusting the prover. Either the holder (with its viewing key) or the auditor (with its auditor key)
+can be the prover, and the contract's storage, entry points, and ciphertexts stay unchanged.
+
+**Non-goals.** The layer proves positive statements only ("this event paid me X"), never completeness ("there are no
+other transfers from Y"); completeness still routes through the auditor. Disclosures are not logged on-chain, since
+that would leak exactly the metadata the protocol hides, and there is no on-chain registry of recipients.
+
+## Disclosure Types
+
+| Disclosure | Produced by | Proves |
+| --- | --- | --- |
+| D-recipient | The holder that received a `Transfer` or `SpenderTransfer` | "This on-chain payment paid me this amount." |
+| D-sender | The originator: the holder for a `Transfer`, the spender for a `SpenderTransfer` | "I sent this on-chain payment, for this amount, to the address in the event." |
+| D-auditor | The auditor of either party | The amount of a transfer in its scope. Variants disclose the sender's post-transfer balance (or allowance) and its opening. |
+| D-balance | The holder | The current spendable balance is at least, or at most, a threshold, or its exact value. |
+| Aggregate | The holder, sender, or auditor | The total across a set of transfers, as an exact value or as at least a threshold, without revealing individual amounts. |
+
+Predicate-style variants reveal no value at all: the proof's validity asserts the predicate. Value-revealing variants
+encrypt the disclosed value to the recipient. D-balance attests to the balance *at verification time*, since a proof
+against a stale commitment fails to verify, whereas the D-auditor balance variant is historical and anchored to a
+specific transfer. The commitment can also change without the holder's involvement, through a compliance clawback or
+forced revocation, in which case the holder proves again against the new one.
+
+## How a Disclosure Works
+
+A recipient publishes a long-lived key pair out of band, for example through web PKI or a certificate. For each
+request, it issues a fresh nonce over an authenticated channel. The recipient key and nonce together bind the
+resulting proof.
+
+```mermaid
+sequenceDiagram
+ participant R as Recipient
+ participant H as Holder wallet
+ participant L as Stellar ledger
+ R->>H: Request: recipient public key, fresh nonce, event reference
+ H->>L: Read the event and account records
+ H->>H: Generate disclosure proof locally
+ H->>R: Proof bundle (off-chain)
+ R->>L: Resolve the event and account records independently
+ R->>R: Build public inputs, verify proof, decrypt value
+```
+
+The **proof bundle** carries only a circuit identifier (which selects the pinned verification key), a reference to each
+disclosed event (transaction hash and event index), the UltraHonk proof, and the disclosed value encrypted to the
+recipient's key. It deliberately carries no event payload, account address, or other public input. The recipient takes
+all of those from the chain itself. D-balance is the exception: with no event to reference, its bundle names the
+disclosing account, which the recipient checks against the one agreed in the request, and its predicate variants
+carry no ciphertext.
+
+The recipient's verifier then runs a fixed sequence of checks, any of which rejects the bundle:
+
+1. Resolve each event reference to exactly one transfer-family event emitted by the deployed confidential token
+ contract, and take its fields verbatim.
+2. Determine the disclosing account from the event, never from the bundle: `to` for D-recipient; `from` (or `spender`)
+ and `to` for D-sender. D-auditor uses the auditor key instead.
+3. Read auxiliary state: the contract's address field and, for D-auditor, the auditor key that was active at the
+ event's ledger, rejecting the bundle if it cannot resolve one. The registry shipped with the library keeps only
+ the current key for each `auditor_id`, so for a transfer made before a rotation, one option is to reconstruct the
+ earlier key from the registry's `AuditorRotated` events, which carry the old and the new key. The registry emits
+ those events, not the token, so they are outside the indexer's scope: beyond the RPC window the verifier needs
+ its own archive of them.
+4. Build the public inputs from chain data, its own key and nonce, and only the encrypted value from the bundle.
+5. Verify the proof against the pinned verification key for the circuit.
+6. Decrypt the disclosed value.
+
+A disclosure leaves no on-chain trace: no transaction, event, or state change. It relies only on public chain data:
+`confidential_balance` for public viewing keys and the spendable commitment, the auditor registry's key lookup, the
+transfer-family events, and the contract address field. That field has no getter, so the verifier reads it from
+instance storage or recomputes it from the contract address. See
+[Verifier Protocol](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/selective-disclosure/protocol.md#verifier-protocol).
+
+
+The verifier's checks are what bind a proof to a specific on-chain event. A verifier that accepts public inputs from
+the bundle instead of resolving them from the chain voids that binding. The verification-key set is a trusted input
+too: a key for a tampered circuit can make false statements verify.
+
+
+### On-Chain Verification
+
+Disclosure proofs are UltraHonk proofs, so a contract could verify them. Doing so publishes the disclosure's existence,
+the recipient, the referenced event, and its timing. That trade-off is worth it only when the result has to gate
+another on-chain action, such as a compliance escrow or a permissioned pool. Such a verifier is a separate protocol,
+outside this specification; the outline is in
+[On-Chain Verification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/selective-disclosure/protocol.md#on-chain-verification).
+
+## Security Properties and Limits
+
+- **Soundness.** A disclosed amount equals the value the on-chain commitment commits to; forging one would require
+ breaking Poseidon2 preimage resistance or discrete log on Grumpkin. The binding to a specific event additionally
+ relies on per-operation salts being unique, which makes each event's ephemeral key unique.
+- **Recipient binding.** Anyone holding a bundle can verify it but cannot decrypt the value; they only learn that some
+ value was disclosed. The nonce prevents reusing the proof for another request.
+- **Cherry-picking.** A holder can disclose some transfers and withhold others. Recipients that need completeness
+ should request a D-auditor disclosure, since the auditor sees every event in its scope.
+- **Out of scope.** Once decrypted, protecting the value is the recipient's responsibility; compelling a holder to
+ disclose is a legal matter, with auditor variants as the cryptographic backstop. Addresses are public anyway, so
+ disclosure protects only amounts.
+- **Spender transfers.** The owner cannot produce a D-sender proof for a `SpenderTransfer`, because the ephemeral key
+ derives from the spender's viewing key. The owner can route through its auditor (D-auditor), or ask the spender for
+ a D-sender proof, which proves that the spender paid. Delegation provenance comes from the `SetSpender` event and
+ the delegation entry.
+- **Older transfers.** A transfer whose ephemeral scalar was not derived deterministically may not be disclosable by
+ its sender. The wallet tests this by recomputing the scalar and comparing it with the event's ephemeral key, and
+ reports a distinct "not disclosable" outcome.
+
+## Implementation Notes
+
+The specification defines four circuits: `disclose_recipient`, `disclose_sender`, and `disclose_auditor`, each with an
+aggregate form over a bounded number of events, and `disclose_balance` in at-least, at-most, and exact-value variants.
+A disclosing wallet keeps its current spendable opening for D-balance and indexes event references so users can pick
+events by date or counterparty. The verifier should be a standalone library, independent of any wallet, that returns
+either the disclosed value or a typed error saying whether proof verification, on-chain state, or decryption failed.
+For aggregates, it must reject bundles in which two references resolve to the same event. See
+[Security and Implementation Notes](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/selective-disclosure/security.md#implementation-notes).
diff --git a/content/stellar-contracts/tokens/confidential/spenders.mdx b/content/stellar-contracts/tokens/confidential/spenders.mdx
new file mode 100644
index 00000000..f1bbe732
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/spenders.mdx
@@ -0,0 +1,201 @@
+---
+title: Spenders
+---
+
+[Source Code](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential)
+
+A spender is an address the owner authorizes to send confidential transfers from a capped allowance
+that expires. Spenders let automated services, payment processors, custodians, or DeFi integrations
+move funds for an owner without the owner sharing their spending key.
+
+Unlike a SEP-41 allowance, which is only a permission to pull from the owner's balance, a confidential
+allowance is **escrowed**. `set_spender` moves the amount out of the owner's spendable balance into a
+separate per-spender commitment. The spender can only ever reach that escrow, so a compromised or
+malicious spender can lose at most the allowance.
+
+## Lifecycle
+
+```mermaid
+stateDiagram-v2
+ [*] --> Active: set_spender
+ Active --> Active: confidential_transfer_from
+ Active --> Expired: ledger > live_until_ledger
+ Active --> [*]: revoke_spender / force_revoke_spender
+ Expired --> [*]: revoke_spender / force_revoke_spender
+```
+
+`force_revoke_spender` exists only in deployments with the compliance extension (see
+[Revoking a Spender](#revoking-a-spender)).
+
+Each `(owner, spender)` pair holds at most one delegation. `set_spender` reverts with
+`DelegationAlreadyExists` while an entry exists for the pair, even if it has expired. To re-delegate to
+the same spender, the owner first calls `revoke_spender` and then `set_spender` again.
+
+## Setting a Spender
+
+```rust
+// `set_spender_data` is the XDR-encoded `SetSpenderData` built by the owner's wallet.
+token_client.set_spender(&owner, &spender, &live_until_ledger, &set_spender_data);
+```
+
+The spender must already be a registered confidential account. The owner's wallet needs the spender's
+spending public key to hand over the key the spender uses to read its allowance.
+
+The owner's proof shows that:
+
+- the allowance comes out of the spendable balance, and the balance covers it;
+- the new spendable commitment and the allowance commitment are well formed;
+- a **delegation viewing key** for this spender is derived correctly and encrypted to the spender's
+ spending key;
+- the owner's auditor receives the escrowed amount, the owner's new balance and its blinding factor, and
+ the blinding factor of the allowance commitment.
+
+The delegation viewing key is derived from the owner's viewing key and the spender's address. It
+reveals only this spender's allowance in this token contract, and it cannot be used to recover the
+owner's viewing key. Because it is escrowed on-chain inside the delegation entry, owner and spender
+never need to exchange keys off-chain.
+
+The owner's spendable balance drops by the allowance as soon as the call succeeds.
+
+## Spending from an Allowance
+
+```rust
+// Authorized by the spender, not the owner.
+token_client.confidential_transfer_from(&spender, &owner, &recipient, &spender_transfer_data);
+```
+
+Only the spender signs. The owner approved in advance at `set_spender`, and that approval lives in the
+delegation entry until it expires or is revoked. The call reverts with `DelegationNotFound` if there is
+no entry, and with `DelegationExpired` once the current ledger is past `live_until_ledger`.
+
+The spender's proof shows that the allowance covers the amount and that the new allowance commitment is
+correct. It also encrypts the amount for the recipient and both auditors, as a direct transfer does.
+The recipient decrypts it as it would a direct
+[confidential transfer](/stellar-contracts/tokens/confidential/operations#confidential-transfer), except
+that the credit arrives as a `SpenderTransfer` event rather than `Transfer`, and the salt to use is the
+event's `sigma_a_new`. A recipient wallet that watches only `Transfer` events misses these credits.
+
+Two more details differ from a direct transfer:
+
+- **Sender-side auditing follows the funds.** The ciphertexts on the sender side go to the **owner's**
+ auditor, not the spender's, and include the remaining allowance after the transfer.
+- **Masks use a replacement salt.** The delegation stores its current salt, `allowance_salt`, which only
+ opens the current allowance. Every encryption mask is keyed to the replacement salt `sigma_a_new` that
+ the spender's wallet picks, and the contract writes it back as the new `allowance_salt`. A reverted
+ call leaves the stored salt unchanged, so the wallet must sample a fresh `sigma_a_new` for every
+ attempt, retries included. The circuit only rejects a value equal to the stored salt; it cannot
+ enforce freshness.
+
+## Revoking a Spender
+
+```rust
+token_client.revoke_spender(&owner, &spender);
+```
+
+Revocation needs no proof. The contract adds the remaining allowance commitment back to the owner's
+spendable commitment and deletes the delegation entry. It works for both active and expired
+delegations.
+
+The `RevokeSpender` event carries the encrypted allowance and its salt. Since the entry is deleted in
+the same call, these event fields are the only place the owner's wallet can read them to learn how much
+came back.
+
+In deployments with the compliance extension, the token admin can also end a frozen account's
+delegation with [`force_revoke_spender`](/stellar-contracts/tokens/confidential/clawback#forced-revocation).
+It needs no authorization from the owner and runs no hook, but it folds the allowance back the same way
+and emits the same `RevokeSpender` event. Owner wallets must therefore handle a `RevokeSpender` they did
+not initiate.
+
+## Expiry
+
+`live_until_ledger` limits how long the spender can spend, but the escrowed funds stay put:
+
+- While `ledger.sequence() <= live_until_ledger`, the spender can call `confidential_transfer_from`.
+- After that, spending reverts, but the escrowed value **stays on-chain** in the delegation entry until
+ the owner calls `revoke_spender`.
+
+`set_spender` does not validate `live_until_ledger`. The value is bound by the owner's authorization,
+but the contract does not compare it with the current ledger and the proof does not cover it. A value
+already in the past creates a delegation that is expired from the start; its escrow comes back only
+through `revoke_spender`. Wallets should check the value before submitting.
+
+Delegations live in persistent storage, not temporary storage, so an expired escrow is never deleted
+automatically. Its TTL is still finite: a new delegation starts with the network's minimum TTL, and the
+library extends it only when an operation reads the entry. An escrow that nobody touches for long
+enough is archived and must be restored before `revoke_spender` or `confidential_transfer_from` can
+use it (see [TTL Management](/stellar-contracts/tokens/confidential/confidential#ttl-management)).
+Wallets should show upcoming expirations and prompt the owner to renew or revoke.
+
+## Owner Operations While Spenders Are Active
+
+The owner's spendable balance and each allowance are separate commitments, so they never need to be
+kept in sync. The owner can transfer, withdraw, merge, or set up more spenders while delegations are
+active. None of these touch an allowance, so a spender's in-flight proof stays valid. Spender
+transfers never touch the owner's spendable balance either. The one owner action that does affect a
+spender is `revoke_spender`, which removes the allowance the spender's next proof would reference.
+
+## Reading Delegation State
+
+`is_spender(account, spender)` returns `true` only if a delegation exists **and** has not expired. It
+returns `false` for a missing, revoked, or expired delegation, so it answers "can this spender spend
+now?", not "is value escrowed?".
+
+`get_spender_delegation(account, spender)` returns the raw `SpenderDelegation` without the expiry
+filter, and reverts if no entry exists. Compare `live_until_ledger` with the current ledger to tell an
+active delegation from an expired one.
+
+| Field | Description |
+| --- | --- |
+| `allowance_commitment` | Commitment to the remaining allowance |
+| `a_tilde` | The remaining allowance, encrypted under the delegation viewing key |
+| `escrowed_dvk` | The delegation viewing key, encrypted to the spender's spending key |
+| `allowance_salt` | Salt of the current allowance state; replaced on every spender transfer |
+| `live_until_ledger` | Last ledger at which the spender may spend |
+
+## Events
+
+Each event's first topic is its name in snake case (`set_spender`, `spender_transfer`, `revoke_spender`),
+followed by the addresses below.
+
+| Event | Topic addresses | Data |
+| --- | --- | --- |
+| `SetSpender` | `account`, `spender` | `live_until_ledger`, `r_e_point`, `sigma`, `b_tilde`, `v_tilde_aud_s`, `b_tilde_aud_s`, `r_tilde_aud_s`, `r_a_tilde_aud_s` |
+| `SpenderTransfer` | `spender`, `from`, `to` | `r_e_point`, `v_tilde`, `sigma_a_new`, `v_tilde_aud_r`, `r_tilde_aud_r`, `v_tilde_aud_s`, `a_tilde_aud_s`, `r_tilde_aud_s` |
+| `RevokeSpender` | `account`, `spender` | `a_tilde`, `allowance_salt` |
+
+- **`SetSpender`** is a spendable-balance checkpoint for the owner, like a `Withdraw` or an outgoing
+ `Transfer`. `b_tilde` and `sigma` let the owner's wallet recover its new spendable balance during
+ recovery. The `…_aud_s` fields are the owner-auditor ciphertexts.
+- **`SpenderTransfer`** carries the amount encrypted for the recipient (`v_tilde`), the ciphertexts for
+ the recipient's auditor and the owner's auditor, and the new salt. It does not carry the new encrypted
+ allowance, which the contract writes only to the delegation entry; read it with
+ `get_spender_delegation`.
+- **`RevokeSpender`** is described in [Revoking a Spender](#revoking-a-spender). It is emitted by both
+ `revoke_spender` and `force_revoke_spender`.
+
+## Spender Wallets
+
+A spender rebuilds its allowance from the delegation entry instead of replaying events. Its wallet
+reads the entry and decrypts `escrowed_dvk` with its own spending key. It then reads the current
+allowance from `a_tilde` and `allowance_salt`, and builds the next `confidential_transfer_from` proof.
+The owner's wallet can read the same allowance, because it can derive the delegation viewing key from
+its own viewing key. See [Wallet Integration](/stellar-contracts/tokens/confidential/wallet-integration).
+
+A spender can see its own allowance, but never the owner's spendable balance.
+
+## Auditing and Disclosure
+
+The owner's auditor sees the escrowed amount at `set_spender`, and the amount and remaining allowance on
+every spender transfer. See [Auditing](/stellar-contracts/tokens/confidential/auditing).
+
+A spender transfer derives its encryption randomness from the spender's own viewing key. The owner
+therefore cannot produce a sender-side
+[selective disclosure](/stellar-contracts/tokens/confidential/selective-disclosure) for it on its own:
+either the spender cooperates, or the owner's auditor discloses the transfer instead.
+
+## Further Reading
+
+- [Set Spender](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/operations/set-spender.md), including [Delegation Key Escrow](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/operations/set-spender.md#delegation-key-escrow)
+- [Spender Transfer](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/operations/spender-transfer.md)
+- [Revoke Spender](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/operations/revoke-spender.md)
+- [Spender Delegation](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/account-state.md#spender-delegation)
diff --git a/content/stellar-contracts/tokens/confidential/wallet-integration.mdx b/content/stellar-contracts/tokens/confidential/wallet-integration.mdx
new file mode 100644
index 00000000..3d503733
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/wallet-integration.mdx
@@ -0,0 +1,203 @@
+---
+title: Wallet Integration
+---
+
+[Specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/README.md)
+
+Apart from the public amounts of deposits and withdrawals, the confidential token contract never sees an amount. It
+stores Pedersen commitments, verifies proofs, and emits encrypted ciphertexts. Every balance, transfer
+amount, or allowance a user sees is computed by client software, the *wallet*. This page describes what a wallet must
+do to operate a confidential account correctly and safely.
+
+
+This page summarizes the SDK specification that accompanies the library. The library does not ship an SDK: it ships
+the on-chain contracts and the Noir circuits. The specification defines obligations rather than an API, so an
+implementation in any language can conform to it.
+
+
+## Why the Client Is Load-Bearing
+
+A balance is a commitment `C = v·G + r·H`. The chain stores the point, but the opening `(v, r)` needed to spend lives
+only in wallet state: if a wallet loses or misaccumulates it, the funds become unspendable, and the contract cannot
+help because it never knew the value. Every amount is client-decrypted, so a decryption bug yields a plausible wrong
+number rather than a visible failure. And all keys, blinding factors, and salts originate in the client, so
+confidentiality reduces to the wallet's key handling and random number generator.
+
+## Keys
+
+All keys of a confidential account derive from a single spending secret `sk`:
+
+| Key | Derived from | Published on-chain | Purpose |
+| --- | --- | --- | --- |
+| Spending key `sk` | The account's root, or imported directly | Spending public key `Y = sk·H` | Witness in every proof the owner produces |
+| Viewing key `vk` | `sk` and the token contract address | Public viewing key `PVK = vk·H` | Decrypts incoming transfers and balance checkpoints; cannot spend |
+| Delegation viewing key `dvk_i` | `vk` and the spender's address | Escrowed to the spender at `set_spender` | Lets one spender track its own allowance, and nothing else |
+
+Because `vk` is bound to the contract address, keys for one deployment reveal nothing about another.
+
+
+The viewing key carries more authority than "read-only" suggests. Besides decrypting balances and incoming transfers,
+it lets its holder recompute the ephemeral scalar of every transfer the account sent, and therefore open those
+transfer commitments retroactively. Never present `vk` as a shareable credential. To prove a single fact to a third
+party, use [Selective Disclosure](/stellar-contracts/tokens/confidential/selective-disclosure).
+
+
+### Deriving the Spending Key
+
+The SDK specification fixes one
+[derivation](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/key-derivation.md#derivation)
+for every client: HKDF-SHA-512 over a *root*, bound to both the token contract and the account address, so one root
+never yields linkable keys on two addresses. What controls the address determines the root:
+
+- **Signer root.** For an ed25519-controlled address, the root is a deterministic
+ [SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md) signature over a message
+ naming the protocol, the contract, and the account, so the user reproduces it from the key they already hold. The
+ wallet must verify the signature against the expected signer and reject signers that do not return the same
+ signature twice, which typically rules out threshold and MPC signers.
+- **Raw root.** For a smart account or other contract address, or a custody stack that cannot sign arbitrary
+ messages, the root is 32 raw bytes from the controlling custody mechanism. Nothing else reproduces it, so the
+ wallet must surface it for backup at creation.
+- **Direct import.** An `sk` sampled directly from a CSPRNG, with no root behind it, stays usable as a first-class
+ account secret and bypasses the derivation. Only an exported copy of `sk` recovers it.
+
+With a signer root, whoever obtains the ed25519 secret gains both view and spend access, and since `register` is
+single-use, `sk` cannot be rotated: remediation means registering a new address and moving the funds. On recovery, a
+wallet derives `sk` for each candidate address and compares `Y = sk·H` against the spending public key stored
+on-chain.
+
+The [key derivation specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/key-derivation.md)
+also requires a wallet to:
+
+- record, per account, which of the three forms produced `sk`, and never offer a recovery path that form cannot
+ satisfy;
+- record the enrolled ed25519 signer instead of assuming the master key, since an address can have several signers,
+ and should warn on sync when that signer has been rotated off the address, which orphans the root;
+- offer `sk` export as a direct-import backup even for signer roots, ideally before the account first receives funds,
+ because a signer can be deterministic within a session but not across sessions;
+- state, where it offers to create an account, that the account's confidentiality is bounded by the secrecy of its
+ signing key.
+
+## Holder Wallet
+
+### Tracking Balances
+
+The wallet keeps one opening for the spendable balance and one for the receiving balance, and updates them from the
+contract's events:
+
+- **Deposits and incoming transfers** add to the receiving opening. Transfer amounts and blindings are recovered by
+ ECDH between the viewing key and the event's ephemeral public key.
+- **Merge** folds the receiving opening into the spendable one and resets the receiving side. `RevokeSpender` and,
+ with the compliance extension, `Clawback` are also folds.
+- **Withdrawals, outgoing transfers, and `set_spender`** *overwrite* the spendable opening from the encrypted balance
+ and salt in the event, so a wallet that missed earlier events still converges on the spendable side.
+- **A self-transfer** is applied on both sides.
+
+Blindings compose in the Grumpkin scalar field, whose modulus `q` is larger than the modulus `r` of Noir's `Field`.
+Fold them as unbounded integers and
+[reduce modulo `q`](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/crypto-core.md#blinding-accumulation)
+only where a blinding leaves the wallet for the curve: a proof witness or a consistency recommit. Reducing modulo `r`
+instead, or at every fold, yields an opening that no longer matches the on-chain point.
+
+Events must be applied in emission order, deduplicated by event id, and idempotently, since a duplicated credit
+inflates the balance. Persisted wallet state is not a cache: once events age out of Stellar RPC, state that no archive
+holds is lost for good (see [Indexing and Recovery](/stellar-contracts/tokens/confidential/indexing)).
+
+### Consistency Checks
+
+After every sync and before building any proof, the wallet re-commits each opening and compares it against the
+on-chain commitment from `confidential_balance`. A missed event, a duplicate, or a bug then shows up as a mismatch
+rather than a wrong balance. The wallet must not spend from mismatched state and should report which balance diverged.
+
+### Salts and Retries
+
+Every attempt samples a fresh salt from a CSPRNG, including retries after a reverted or dropped transaction. The salt
+is the only freshness input to the operation's blinding, its ephemeral key (derived deterministically from the
+originator's viewing key and the salt), and every encryption mask. Reusing one is therefore a confidentiality failure,
+not only a linkability one. The contract cannot enforce freshness, so wallets must never cache, reuse, or cycle salts.
+
+### In-Flight Operations and Merging
+
+A wallet may show the projected balance right after submission, but must reconcile it against the emitted event
+before treating it as confirmed.
+
+Received funds become spendable only after a merge. Merge needs no proof and is cheap, so a wallet should merge
+automatically, or prompt, before a spend the spendable balance cannot cover. Regular merges also shorten the replay
+window for recovery.
+
+### The Unspendable-Blinding Case
+
+After a proofless fold (merge, revocation, or clawback), the spendable blinding can, with negligible probability (about
+2⁻¹²⁷ per fold), land outside the range a proof can witness. The commitment stays valid and consistency checks pass,
+but no proof can be built against it until the next merge that folds in an inbound confidential transfer; deposits
+alone do not resolve it. Wallets should test for this before proving and surface it as a distinct state with that
+recovery path.
+
+## Proving and Submission
+
+- **Witness assembly.** Assemble public inputs in exactly the order the contract does: a permutation of two same-typed
+ inputs proves a different statement. Leave inputs the contract loads from its own state out of the payload.
+- **Canonical encoding.** Every field value the wallet emits, in a payload, proof input, or persisted state, must be
+ the [canonical](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/crypto-core.md#canonicality)
+ representative below `r`. Reject non-canonical values at the wallet's own boundary: the contract rejects them too,
+ but by then local state may already hold bytes that recovery cannot reproduce.
+- **Toolchain pinning.** Generate proofs with the toolchain and flags pinned in
+ [`circuits/vks/README.md`](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/circuits/vks/README.md),
+ notably a Keccak Fiat-Shamir transcript (many backends default to Poseidon2, which verifies locally and fails
+ on-chain) with zero-knowledge mode off. That recipe is provisional and will be finalized together with the
+ verifier, including the zero-knowledge setting (see
+ [Generating Proofs](/stellar-contracts/tokens/confidential/proof-system#generating-proofs) for what the current
+ setting implies). Check that the circuit's verification key matches the deployed one before submitting.
+- **Where proving runs.** Keep the prover pluggable (in-process WASM, native, or remote). Remote proving discloses the
+ spending key, balance, and amount to the prover, so it must be explicit opt-in, never a fallback.
+- **Submission.** Authorize with the principal the entry point expects, which for `confidential_transfer_from` is the
+ spender, not the owner (see [Operations](/stellar-contracts/tokens/confidential/operations)). Simulating first
+ catches stale commitments, frozen accounts, and expired delegations without a fee.
+- **Failures.** Distinguish proof verification failures (check the verification key and witness) from state
+ mismatches (re-sync and rebuild): they look alike on the wire but have opposite remedies. A rotation of an auditor
+ key or of the verifier between proving and submission also invalidates an in-flight proof; the wallet rebuilds it
+ against the new key with a fresh salt (see
+ [Key Rotation](/stellar-contracts/tokens/confidential/auditing#key-rotation)).
+
+## Roles
+
+The specification separates the holder, spender, auditor, and disclosure-recipient roles, each holding distinct key
+material and structurally unable to exceed its capability. A spender wallet, for example, rebuilds allowance state
+from the on-chain delegation entry and must never reach the owner's spendable balance (see
+[Spenders](/stellar-contracts/tokens/confidential/spenders)), and an auditor client must never be able to build a
+spending witness (see [Auditing](/stellar-contracts/tokens/confidential/auditing)).
+
+## Requirements
+
+- **Secrets stay local.** The root, keys, ephemeral scalars, and cached openings never leave the device, except under
+ opted-in remote proving, and never reach logs, telemetry, or crash reports.
+- **Storage at rest.** A persisted opening is as sensitive as its amount, and a persisted root is equivalent to the
+ funds, so encrypt both. Never persist a signer root; re-derive it on demand.
+- **Randomness and supply chain.** Use a CSPRNG and fail rather than fall back. Pin verification keys and the proving
+ toolchain; never fetch them at runtime from a mutable location.
+- **Host compromise.** Software cannot defend against a compromised host, and a hardware device that does not prove
+ internally must expose `sk` to the host, since `sk` is a proof witness.
+- **Scale.** Proving targets single-digit seconds. Inbound volume is limited only by transaction fees, so do not size
+ storage on the user's own activity.
+
+Conformance is defined against the language-agnostic fixtures in
+[`circuits/lib/testdata`](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/confidential/circuits/lib/testdata):
+an implementation reproduces every fixture byte-for-byte, and runs the real compiled circuits against its own
+witnesses, including tampered ones that must be rejected. See
+[Conformance and Versioning](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/requirements.md#conformance-and-versioning).
+
+
+Two vectors the specification
+[requires](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/conformance.md#vectors-this-specification-requires)
+do not exist yet: the ephemeral-scalar derivation, and the chain from a root (including its SEP-53 signature) to `sk`,
+`vk`, `Y`, and `PVK`. Until they land, no fixture checks that two clients derive the same account from the same
+backup, or the same ephemeral scalars, so transfers sent from one client may not be disclosable from another.
+
+
+## UX Considerations
+
+Confidential transfers should feel like regular ones, with two visible differences: **proof generation latency** when
+sending, withdrawing, or delegating, and **the merge step** before received funds become spendable, which the wallet
+should make nearly automatic. Keys, commitments, proofs, salts, and auditor ciphertexts stay behind the wallet, and
+the holder selects an auditor once at registration. Incoming transfers never invalidate an in-flight spend proof, and
+an expired delegation still holds its escrow until the owner revokes it, so wallets should surface upcoming
+expirations.
diff --git a/src/navigation/stellar.json b/src/navigation/stellar.json
index 9c341127..66ff9563 100644
--- a/src/navigation/stellar.json
+++ b/src/navigation/stellar.json
@@ -67,6 +67,80 @@
"type": "page",
"name": "Vault",
"url": "/stellar-contracts/tokens/vault/vault"
+ },
+ {
+ "type": "folder",
+ "name": "Confidential",
+ "index": {
+ "type": "page",
+ "name": "Confidential Token",
+ "url": "/stellar-contracts/tokens/confidential/confidential"
+ },
+ "children": [
+ {
+ "type": "folder",
+ "name": "Operations",
+ "index": {
+ "type": "page",
+ "name": "Operations",
+ "url": "/stellar-contracts/tokens/confidential/operations"
+ },
+ "children": [
+ {
+ "type": "page",
+ "name": "Spenders",
+ "url": "/stellar-contracts/tokens/confidential/spenders"
+ }
+ ]
+ },
+ {
+ "type": "page",
+ "name": "Auditing",
+ "url": "/stellar-contracts/tokens/confidential/auditing"
+ },
+ {
+ "type": "folder",
+ "name": "Compliance",
+ "index": {
+ "type": "page",
+ "name": "Compliance",
+ "url": "/stellar-contracts/tokens/confidential/compliance"
+ },
+ "children": [
+ {
+ "type": "page",
+ "name": "Clawback",
+ "url": "/stellar-contracts/tokens/confidential/clawback"
+ }
+ ]
+ },
+ {
+ "type": "page",
+ "name": "Proof System",
+ "url": "/stellar-contracts/tokens/confidential/proof-system"
+ },
+ {
+ "type": "folder",
+ "name": "Wallet Integration",
+ "index": {
+ "type": "page",
+ "name": "Wallet Integration",
+ "url": "/stellar-contracts/tokens/confidential/wallet-integration"
+ },
+ "children": [
+ {
+ "type": "page",
+ "name": "Indexing and Recovery",
+ "url": "/stellar-contracts/tokens/confidential/indexing"
+ },
+ {
+ "type": "page",
+ "name": "Selective Disclosure",
+ "url": "/stellar-contracts/tokens/confidential/selective-disclosure"
+ }
+ ]
+ }
+ ]
}
]
},