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..e9443e99
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/auditing.mdx
@@ -0,0 +1,221 @@
+---
+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 uses the same auditor for 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 uses the same two-auditor model. The table above shows what each auditor learns at
+`set_spender` and on each spender transfer: the recipient's auditor sees a spender transfer like any other, and the
+owner's auditor tracks the allowance from the escrowed amount onward. Two things are specific to delegations:
+
+- **Revocation publishes no auditor ciphertext.** On `revoke_spender`, 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.
+- **Allowance openings exist only in events.** Each one comes from the event that wrote that state, not from
+ contract storage, so an auditor that misses a delegation event can't 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
+}
+```
+
+The registry depends on `stellar-confidential`, like the token (see
+[Installation](/stellar-contracts/tokens/confidential/confidential#installation)). The example below gates all writes
+behind a `manager` role:
+
+```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.** It can read individual amounts right away, but it holds no full opening of the
+ spendable balance until the account's next checkpoint (withdrawal, outgoing transfer, or `set_spender`), and none
+ of an allowance until the delegation's next state-changing event. Older state can break the spendable opening
+ again after that checkpoint: merging transfers received before the rotation, or revoking a delegation last
+ written before it, leaves it unknown until the next checkpoint. A receiving balance that holds pre-rotation
+ transfers opens only after the next merge. Until it holds both openings, the new key's holder can't build a
+ clawback proof for the account.
+
+## 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.
+
+Key custody, rotation planning, and how to use the access an auditor key grants are covered in
+[Best Practices for Auditors](/stellar-contracts/tokens/confidential/best-practices/auditors).
+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/best-practices/account-holders.mdx b/content/stellar-contracts/tokens/confidential/best-practices/account-holders.mdx
new file mode 100644
index 00000000..9241a7d1
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/best-practices/account-holders.mdx
@@ -0,0 +1,82 @@
+---
+title: Best Practices for Account Holders
+---
+
+These practices are for anyone who holds a confidential account, and for the wallet developers who choose defaults
+on their behalf. The wallet carries out most of them: [Wallet Integration](/stellar-contracts/tokens/confidential/wallet-integration)
+states what a wallet must do, and this page states what a holder should do and why. The
+[Privacy Model](/stellar-contracts/tokens/confidential/privacy-model) explains what the chain makes public.
+
+## Before Registering
+
+- **Choose the auditor deliberately.** The `auditor_id` picked at registration can never change. That auditor sees
+ the amount of every transfer the account sends or receives and, from the account's first spend onward, its
+ balances. The choice is public too. Find out who holds the auditor key and under which rules it may be used.
+- **Know what your keys derive from.** When the wallet derives the account's keys from its Stellar signing key,
+ that key controls both spending and viewing, and the account's keys can't be rotated: registration is single-use,
+ so replacing a compromised signing key means registering a new address and moving the funds. Export the spending
+ secret as a backup before the account receives funds.
+- **Don't expect anonymity.** Your address appears in every operation you take part in. The confidential token hides
+ amounts and balances, not whom you deal with.
+
+## Keeping Amounts Private
+
+- **Keep value inside the confidential layer.** Deposit and withdrawal amounts are public, and a balance built only
+ from them is public too. Pay other holders with confidential transfers instead of withdrawing to the underlying
+ token and depositing again.
+- **Don't mirror amounts at the boundary.** Withdrawing exactly what you received, or depositing exactly what you are
+ about to send, reveals the hidden amount. Prefer common amounts for deposits and withdrawals, and don't tie their
+ timing to specific transfers.
+- **Expect counterparties to know.** The recipient of a transfer learns its amount, and so does the auditor on each
+ side.
+- **Never share your viewing key.** It reveals your whole history and the openings of every transfer you sent, and
+ since it is bound to your account at registration, it can't be revoked. To prove one fact, such as the amount of a
+ payment or that a balance is above a threshold, use
+ [Selective Disclosure](/stellar-contracts/tokens/confidential/selective-disclosure).
+- **Prove locally.** A remote prover sees your spending key, your balance, and the amount of every proof it builds.
+ Opt in to one only if you would trust it with your entire confidential history.
+
+## Protecting Your Balance
+
+The chain stores only commitments. The opening of each balance, which every spend needs, lives in your wallet, and
+the wallet rebuilds it from your keys and your account's event history when it loses its local state.
+
+- **Back up what recovers your keys.** That is the Stellar signing key for an account whose keys derive from it, and
+ the root or the exported spending secret otherwise. Losing it means losing the funds.
+- **Keep old signing keys.** If you rotate the signer that controls your address, keep the previous key: an account
+ whose keys derive from it can't be re-derived without it.
+- **Sync regularly.** Stellar RPC keeps events only for a limited window. A wallet that relies on RPC alone must sync
+ at least once per window, because credits that age out before it applies them are lost unless an
+ [archive](/stellar-contracts/tokens/confidential/indexing) holds them. Prefer wallets that use one.
+- **Merge regularly.** Received funds become spendable only after a merge, and regular merges also shorten the history
+ a recovery has to replay.
+- **Resync before spending after a warning.** A wallet that reports a mismatch between its local balance and the
+ on-chain commitment has missed or misapplied an event. Spending from that state fails.
+
+### Recovery Scenarios
+
+| Situation | What recovers the account |
+| --- | --- |
+| Device lost, keys derived from the signing key | The signing key, plus the event history from RPC or an archive |
+| Device lost, keys from a raw root or an imported secret | The backup of that root or secret, plus the event history. Without the backup, the funds can't be spent. |
+| Signing key rotated off the address and discarded | Only a previously exported spending secret |
+| Wallet unsynced for longer than the RPC window, no archive | Nothing, for the credits in the events that aged out |
+
+
+Recover with the same wallet software that created the account where possible. The conformance vectors for key
+derivation don't exist yet, so two wallets are not guaranteed to derive the same account from the same backup. See
+[Requirements](/stellar-contracts/tokens/confidential/wallet-integration#requirements).
+
+
+## Working with Spenders
+
+- **Escrow only what the spender needs, for as long as it needs it.** The allowance is all a compromised spender can
+ take, and the expiry bounds how long it can take it.
+- **Check the expiry before delegating.** `set_spender` accepts a `live_until_ledger` that has already passed, which
+ creates a delegation that is expired from the start.
+- **Revoke expired delegations.** Expiry stops the spender but doesn't return the escrow; only `revoke_spender` does.
+ An escrow that nobody touches for long enough is also archived, and must be restored before it can be revoked.
+- **Remember that delegations are public.** Each owner-spender pair, its expiry, and every transfer the spender makes
+ on your behalf are visible on-chain.
+- **Plan how to disclose spender transfers.** You can't produce a sender-side disclosure for a transfer your spender
+ made: ask the spender, or your auditor. See [Spenders](/stellar-contracts/tokens/confidential/spenders#auditing-and-disclosure).
diff --git a/content/stellar-contracts/tokens/confidential/best-practices/administrators.mdx b/content/stellar-contracts/tokens/confidential/best-practices/administrators.mdx
new file mode 100644
index 00000000..f97bba78
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/best-practices/administrators.mdx
@@ -0,0 +1,80 @@
+---
+title: Best Practices for Administrators
+---
+
+These practices are for whoever deploys a confidential token and holds its privileged roles: the token's admin and
+compliance roles, the managers of the verifier and auditor registries, and the issuer of the underlying asset.
+
+The [testnet deployment guide](https://github.com/OpenZeppelin/stellar-contracts/blob/main/examples/confidential/README.md)
+walks through a complete deployment with the Stellar CLI. This page covers what changes between that guide and a
+production deployment.
+
+## Roles and Governance
+
+- **Give every role its own account.** The testnet guide lets one account hold every role. In production, keep the
+ token's admin, freezing, configuration, and seizure roles on separate accounts, and keep the registries' managers
+ apart from the token's admin, so that no single key can both reconfigure the token and seize funds.
+- **Prefer immutability.** The strongest posture fixes every verification key and the token's links to its
+ registries at deployment: register the keys in the verifier's constructor, make `update_verification_key` always
+ panic, and expose no path that calls `set_verifier` or `set_auditor` after construction. See
+ [Updating a Verification Key](/stellar-contracts/tokens/confidential/proof-system#updating-a-verification-key).
+- **Gate what stays mutable.** A verification key update, a new verifier or auditor registry, and an auditor key
+ rotation each change which proofs the token accepts or who can read its ciphertexts. Put each behind a multisig
+ and a [timelock](/stellar-contracts/governance/timelock-controller), and publish enough material for anyone to
+ check the change before it activates.
+- **Remember that wallets pin verification keys.** Wallets must not fetch keys at runtime, so a key update breaks
+ every wallet that hasn't shipped the new key, and every update fails the proofs in flight.
+- **Monitor configuration events.** Watch `verification_key_updated` on the verifier registry, `auditor_rotated` on
+ the auditor registry, and `verifier_set`, `auditor_set` and `compliance_config_changed` on the token. An event no
+ one planned is an incident.
+
+## Deploying
+
+- **Choose the underlying token carefully.** It must meet the
+ [underlying token requirements](/stellar-contracts/tokens/confidential/confidential#underlying-token-requirements).
+ Over a Stellar Asset Contract (SAC), the issuer can freeze or claw back the token contract's pooled balance, which
+ affects every holder at once.
+- **Register the pinned keys, then check them.** Register the `.vk.bin` file of every circuit the deployment uses,
+ from the release you build against, and the `Clawback` key only with the clawback extension. Read each key back
+ with `get_verification_key` and compare it byte for byte with the pinned file. See
+ [Registering Verification Keys](/stellar-contracts/tokens/confidential/proof-system#registering-verification-keys).
+- **Never reuse example keys.** The testnet guide registers the Grumpkin generator as its auditor key. Its secret is
+ `1`, so anyone can decrypt everything encrypted to it. Register only keys generated by the auditor that will hold
+ them.
+- **Restrict auditor selection** if the deployment has designated auditors. An account's `auditor_id` never changes,
+ so `register` is the only point where a deployment controls who may see an account's activity. See
+ [Restrict Auditor Selection](/stellar-contracts/tokens/confidential/compliance#restrict-auditor-selection).
+
+## Supporting Holders and Auditors
+
+- **Publish what users need to verify the deployment.** That means the addresses of the token and both registries,
+ the circuit source revision, the proving toolchain versions, the verification key files, the protocol
+ documentation version, and the auditor ids holders may choose, with who operates each.
+- **Run or contract at least two independent archives** that meet the
+ [indexer specification](/stellar-contracts/tokens/confidential/indexing). Without one, holders cannot recover once
+ events age out of Stellar RPC, and auditors lose the openings that exist only in events.
+- **Serve the whole contract stream** alongside per-account queries, so wallets and auditors can sync without telling
+ the archive which accounts they follow.
+- **Announce auditor key rotations.** A rotation fails every proof in flight, and the new key holds no openings until
+ each account's next checkpoint, so plan it with the auditor. See
+ [Key Rotation](/stellar-contracts/tokens/confidential/auditing#key-rotation).
+
+## Compliance Operations
+
+- **Wire the hooks before enabling seizure.** A token that implements `ConfidentialClawback` needs
+ `ComplianceHooks`, or a custom `Hooks` that forwards every callback it doesn't customize. Otherwise a freeze
+ blocks nothing and the target can move its balance before the seizure lands.
+- **Enable `sac_passthrough` only over a SAC.** Over any other token, every checked operation fails.
+- **Decide who may deposit for whom.** By default anyone can deposit into any registered account. Unsolicited
+ deposits complicate an auditor's bookkeeping, while outside payers may be part of your use case. The
+ [Compliance](/stellar-contracts/tokens/confidential/compliance#customizing-the-hooks-trait) page has hooks for
+ both directions.
+- **Follow the seizure order.** Freeze the account, find its delegations from its `SetSpender` and `RevokeSpender`
+ events, revoke each with `force_revoke_spender`, and only then ask the auditor for the proof. Prefer a
+ `destination`. With `destination = None`, seize first and extract the surplus at the SAC afterwards, never the
+ other way round. See [Clawback](/stellar-contracts/tokens/confidential/clawback#seizure-flow).
+- **Plan the SAC flags before the first deposit.** Extracting a surplus with the SAC's own `clawback` requires
+ `AUTH_CLAWBACK_ENABLED` on the issuer before the token contract's first deposit. Enabling it later doesn't apply to
+ the contract's existing balance.
+- **Expect compliance actions to be public.** Freezes and clawbacks name the account, and a clawback publishes its
+ amount and destination.
diff --git a/content/stellar-contracts/tokens/confidential/best-practices/auditors.mdx b/content/stellar-contracts/tokens/confidential/best-practices/auditors.mdx
new file mode 100644
index 00000000..35f8b4e8
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/best-practices/auditors.mdx
@@ -0,0 +1,78 @@
+---
+title: Best Practices for Auditors
+---
+
+These practices are for whoever holds the secret of an auditor key registered in an auditor registry.
+[Auditing](/stellar-contracts/tokens/confidential/auditing) explains what an auditor can see and what its software
+has to do; this page covers operating that capability safely.
+
+The protocol trusts the auditor with two things: protecting its secret key, and using its access only on legitimate
+request. Nothing on-chain enforces either.
+
+## Key Management
+
+- **Generate the key yourself.** Generate the key pair with an auditor client, from a cryptographically secure
+ random source, and hand the registry manager only the public point. Never accept a key generated by someone else,
+ or a test key such as the one in the testnet deployment guide, whose secret is `1`.
+- **Custody it at least as carefully as a viewing key.** The key decrypts every ciphertext addressed to it, for every
+ account bound to its id. The openings it yields are self-verifying: anyone who receives one can check it against
+ the chain.
+- **Keep every historical key.** Each key decrypts only the events produced while it was active, so a replaced key is
+ still needed for as long as its events may be reviewed. The registry's `auditor_rotated` events record when each
+ key took over.
+
+
+**Losing the auditor key is permanent.** Ciphertexts can't be re-encrypted to a new key, so everything encrypted to
+a lost key stays unreadable. Until a replacement key has collected fresh openings, the auditor also cannot produce
+[clawback](/stellar-contracts/tokens/confidential/clawback) proofs for its accounts. Back up each key before it is
+registered.
+
+
+- **Rotate immediately if the key is compromised.** A compromised key reveals the amounts and balances of every
+ bound account for as long as it stays active, but it can't spend and it can't read events from before it became
+ active. Rotation protects only what comes after it.
+- **Expect a gap after every rotation.** A rotation fails every proof in flight, and the new key holds no openings
+ until each account's next withdrawal, outgoing transfer, or `set_spender`. See
+ [Key Rotation](/stellar-contracts/tokens/confidential/auditing#key-rotation).
+
+## Running the Auditor Client
+
+- **Ingest continuously and archive everything.** The blinding of each allowance is escrowed only in the event that
+ wrote it, so an event missed now can't be recovered later. Run or contract an archive that meets the
+ [indexer specification](/stellar-contracts/tokens/confidential/indexing), rather than relying on Stellar RPC's
+ limited window.
+- **Check every opening against the chain.** Re-commit each opening and compare it with the stored commitment. A
+ mismatch means a missed, reordered, or pruned event. Where you audit both parties of a transfer, also check that
+ the amount decrypts to the same value on both channels.
+- **Don't reveal your scope.** Asking an RPC provider or a third-party archive for specific accounts tells it which
+ accounts you audit. Fetch the whole contract stream, or run your own node and archive.
+- **Use software built for the auditor role.** An auditor client must not be able to build a spending witness, and
+ must keep its key and openings out of logs and crash reports. See the
+ [Auditor Client specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/sdk/auditor-client.md).
+
+## Using Access
+
+- **Decrypt only on legitimate request.** The key makes everything in scope readable; whether a given read is
+ justified is a policy question. Keep a record of each access and its reason.
+- **Answer narrow questions narrowly.** Hand a requester only what it asked for. Where the deployment supports
+ [selective disclosure](/stellar-contracts/tokens/confidential/selective-disclosure), a D-auditor proof
+ discloses one transfer's amount to one recipient, bound to its request, instead of decrypted records that can be
+ passed on.
+- **Attest to completeness only with a gap-free history.** A holder can disclose some transfers and withhold others,
+ so requests for completeness come to the auditor. Confirm that the archive reports a complete history for the whole
+ range first.
+
+## Supporting Seizures
+
+In a deployment with the [clawback extension](/stellar-contracts/tokens/confidential/clawback), the auditor produces
+the proof that fixes the seized amount and its destination.
+
+- **Prove only after the freeze and every forced revocation.** Any change to the account's commitments between
+ proving and submission fails the proof, and each revocation changes the spendable balance.
+- **Verify both openings and read the current `clawback_nonce` before proving.** A proof built on stale state or an
+ old nonce is rejected.
+- **Check the destination.** The proof binds the destination you were asked for, and the admin can't change it. On
+ a SAC, the destination has to be authorized for the asset (for a `G...` account, an authorized trustline), or the
+ whole seizure reverts.
+- **Apply the `Clawback` event like a merge followed by a debit** of its public amount. It carries no encrypted
+ balance, so it is not a checkpoint.
diff --git a/content/stellar-contracts/tokens/confidential/clawback.mdx b/content/stellar-contracts/tokens/confidential/clawback.mdx
new file mode 100644
index 00000000..4360dd98
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/clawback.mdx
@@ -0,0 +1,196 @@
+---
+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.
+
+Knowing an account's balance isn't enough to build the proof. An account whose history is only registration,
+deposits, and merges has balances anyone can reconstruct from public events, so the circuit also requires the
+auditor's secret key, and only the auditor can prove a seizure.
+
+## 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,
+ ) {
+ // `data` is an XDR-encoded `ClawbackData { proof }`, not the raw proof bytes.
+ // A malformed envelope reverts with `InvalidData`.
+ 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 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`.
+
+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..53cf7fa6
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/compliance.mdx
@@ -0,0 +1,427 @@
+---
+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 {
+ /// Address of an external policy contract, or `None` for no policy check.
+ pub policy: Option,
+ /// When `true`, the check on every fund-holding address also asks the
+ /// underlying SAC's `authorized()` view.
+ pub sac_passthrough: bool,
+}
+```
+
+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 {
+ /// `token` is the calling confidential token, so one registry can serve several
+ /// tokens and still apply per-token rules.
+ 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.
+
+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. Besides the examples below,
+custom hooks can enforce 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.
+
+
+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,
+};
+
+// Wire it with `type Hooks = PermissiveDepositHooks;`.
+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);
+ }
+}
+```
+
+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) {
+ // `DataKey` and `DeploymentError` are defined by the deployment, which fills the
+ // approved list at construction or through an admin entry point.
+ 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...
+}
+```
+
+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.
+
+## 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)).
+
+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..099b025b
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/confidential.mdx
@@ -0,0 +1,203 @@
+---
+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.
+
+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.
+
+## 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), the Noir circuits, and example contracts under
+[`examples/confidential`](https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/confidential): the
+verifier and auditor registries, an allowlist policy, a token with the compliance extension, and a factory. 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](/stellar-contracts/tokens/confidential/privacy-model#zero-knowledge-setting). |
+| **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`. Add it to the
+contract's `Cargo.toml`, pinned to an exact version as the library recommends for all its crates:
+
+```toml
+[dependencies]
+stellar-confidential = "=0.8.0"
+```
+
+The contract must use a `soroban-sdk` version compatible with the one `stellar-confidential` depends on.
+
+## 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.
+
+The [testnet deployment guide](https://github.com/OpenZeppelin/stellar-contracts/blob/main/examples/confidential/README.md)
+walks through these steps with the Stellar CLI and the example contracts, and also deploys an allowlist policy and
+the compliance extension. [Best Practices for Administrators](/stellar-contracts/tokens/confidential/best-practices/administrators)
+covers what changes for a production deployment.
+
+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;
+ // Every entry point and read method uses the trait's default implementation.
+}
+```
+
+See [Operations](/stellar-contracts/tokens/confidential/operations) for what each entry point does.
+
+## 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
+
+- **Proofs** use the non-zero-knowledge flavor of UltraHonk, the only one the on-chain verifier supports. Soundness
+ is unaffected. Amount and balance privacy rests on how the circuits are built instead, and a proof lowers the
+ security of the prover's long-term key from about 127 to about 100 bits. The proving recipe stays provisional
+ until the zero-knowledge flavor is supported. See
+ [Zero-Knowledge Setting](/stellar-contracts/tokens/confidential/privacy-model#zero-knowledge-setting).
+- **Soundness** rests on the verification keys in the verifier registry matching the intended circuits. A wrong key
+ makes the contract accept forged proofs. See [Proof System](/stellar-contracts/tokens/confidential/proof-system).
+- **Privacy** covers amounts and balances, not addresses or timing, and some balances can be inferred from public
+ flows. See [Privacy Model](/stellar-contracts/tokens/confidential/privacy-model).
+- **Operations** such as key custody, backups, governance, and seizures are covered by role in the Best Practices
+ for [account holders](/stellar-contracts/tokens/confidential/best-practices/account-holders),
+ [administrators](/stellar-contracts/tokens/confidential/best-practices/administrators), and
+ [auditors](/stellar-contracts/tokens/confidential/best-practices/auditors).
+
+## 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..08ce41fc
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/indexing.mdx
@@ -0,0 +1,147 @@
+---
+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 can't reconstruct the openings needed to spend them. Without a conforming archive, treat
+wallet state as unrecoverable once its events age out of RPC. A wallet with RPC-only access must sync at least once
+per retention window and warn the user when it hasn't: 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
+ T0["T0"] -->|"replay receiving side"| CP[Latest checkpoint]
+ CP -->|"replay both sides"| 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. Per-account queries tell the indexer which accounts a client follows (see
+[Trust Model](#trust-model)), so an indexer should offer both.
+
+**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 its contents tell a curious indexer nothing a
+ chain observer doesn't already know. Its queries do: a client that asks for one account's history tells the
+ indexer which account it follows, from which network address, and when, and an auditor's queries reveal which
+ accounts it audits. Clients that need to hide this fetch the whole contract stream and filter locally, or run
+ their own archive.
+- **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 exists only in
+the `SetSpender` or `SpenderTransfer` event that wrote it, so a missed or unarchived event leaves the auditor without
+that opening. See [Running an Auditor](/stellar-contracts/tokens/confidential/auditing#running-an-auditor).
diff --git a/content/stellar-contracts/tokens/confidential/operations.mdx b/content/stellar-contracts/tokens/confidential/operations.mdx
new file mode 100644
index 00000000..05629439
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/operations.mdx
@@ -0,0 +1,243 @@
+---
+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, and an account can receive any number of transfers
+without the owner doing anything. The cost is one extra step: received funds must be merged before they
+can be spent. See
+[Griefing Resistance](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/security.md#griefing-resistance).
+
+## 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`, which decides who receives the encrypted copies of this account's
+activity (see [Auditing](/stellar-contracts/tokens/confidential/auditing)). It can never change. The id must
+exist in the auditor registry, or the call reverts with the registry's `AuditorNotRegistered`. Beyond that,
+the core lets an account pick any auditor; deployments that need to restrict the choice do so in
+`Hooks::on_register` (see [Restrict Auditor Selection](/stellar-contracts/tokens/confidential/compliance#restrict-auditor-selection)).
+
+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
+// `depositor` authorizes the underlying SEP-41 transfer and needs no confidential account;
+// `recipient` must be registered. A negative amount reverts with `NegativeAmount`.
+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.
+
+## Merge
+
+```rust
+// Authorized by `owner`. Adds the receiving commitment to the spendable one and resets the
+// receiving balance to zero.
+token_client.merge(&owner);
+```
+
+Merge 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.
+
+The owner's wallet knows the new opening because it has tracked every deposit and transfer credited to
+the receiving balance. Wallets should merge, or prompt the owner to, before a spend the spendable
+balance can't cover.
+
+## Confidential Transfer
+
+```rust
+// `transfer_data` is the XDR-encoded `TransferData` built by the sender's wallet.
+// Replaces the sender's spendable commitment and adds the transfer commitment to the
+// recipient's receiving balance.
+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.
+
+```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, although public
+deposits and withdrawals can let them infer some of it (see
+[Privacy Model](/stellar-contracts/tokens/confidential/privacy-model#what-observers-can-infer)).
+
+## Withdraw
+
+```rust
+// `withdraw_data` is the XDR-encoded `WithdrawData` built by the owner's wallet.
+// Replaces the owner's spendable commitment and sends the underlying tokens to
+// `destination`, which can be any address.
+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 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);
+// Reverts with `InvalidData` if `data` doesn't decode, and with `NonCanonicalEncoding`
+// if a field or point coordinate isn't the canonical encoding of its field element.
+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.
+
+## 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
+
+Storage holds only commitments, so events are how wallets, auditors and indexers learn what happened.
+[What Each Event Reveals](/stellar-contracts/tokens/confidential/privacy-model#what-each-event-reveals) lists
+every event with its public and encrypted fields.
+
+A wallet that loses its local state rebuilds both balances from these events. Stellar RPC keeps events
+only for a limited window, so recovery depends on a durable archive; see
+[Indexing and Recovery](/stellar-contracts/tokens/confidential/indexing) for the procedure.
+
+## Read Methods
+
+```rust
+// Returns the `ConfidentialAccount` below; reverts if `owner` isn't registered.
+let account = token_client.confidential_balance(&owner);
+let active = token_client.is_spender(&owner, &spender);
+let delegation = token_client.get_spender_delegation(&owner, &spender);
+```
+
+| 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
+
+A proof is 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 updates a
+[verification key](/stellar-contracts/tokens/confidential/proof-system#updating-a-verification-key) while a
+proof is in flight, the call reverts with `InvalidProof` and nothing changes. The wallet rebuilds the proof
+and resubmits.
+
+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, so each
+attempt needs a fresh one, retries included. Reusing a salt would let an observer link the failed attempt
+to the retry, and a retry with a different amount would reveal the difference between the two amounts. The
+contract can't enforce freshness, so this 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/privacy-model.mdx b/content/stellar-contracts/tokens/confidential/privacy-model.mdx
new file mode 100644
index 00000000..d0faa90c
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/privacy-model.mdx
@@ -0,0 +1,160 @@
+---
+title: Privacy Model
+---
+
+[Specification](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/docs/protocol/security.md#privacy-properties)
+
+The confidential token hides how much an account holds and how much it sends. It does not hide who transacts with
+whom, when, or which operation they call. Most of what is public about amounts comes from wrapping a public token, so
+this page starts there. It then covers what each party can learn, what every event publishes, what a proof can reveal
+(see [Zero-Knowledge Setting](#zero-knowledge-setting)), what an observer can infer by combining events, and what can
+be learned off-chain.
+
+## Boundary with the Underlying Token
+
+The confidential token is a layer on top of an existing SEP-41 token, which stays transparent. Value enters the layer
+only through `deposit` and leaves it only through `withdraw` or a clawback, and every crossing settles on the
+underlying token in public. Two things follow from this boundary:
+
+- **Every crossing publishes its amount.** Deposit, withdrawal and clawback amounts are public. A clawback counts as a
+ crossing because the seized amount has to be reconciled against the pool of underlying tokens the contract holds.
+ [What Observers Can Infer](#what-observers-can-infer) shows how these amounts can reveal balances and transfers.
+- **The underlying token's history stays public.** Where a deposit's tokens came from, and where withdrawn tokens go
+ next, can be traced on the underlying token like any other transfer. Both ends of an account's confidential
+ activity are linked to its public history.
+
+A native confidential token, whose balances are confidential from issuance, avoids most of this. Value is minted
+straight into confidential balances and never exists in transparent form, so there are no deposits or withdrawals to
+watch, no public history to trace, and every holder is on the private side. Amounts are public only where the issuer
+chooses, typically at minting and burning. This library implements only the wrapped model.
+
+The boundary doesn't explain everything. The transaction graph, timing, auditor choice, transaction sources, freezes,
+and off-chain visibility described below follow from addresses being public and from how clients handle keys and data,
+so a native token inherits them. Outside privacy, the
+[underlying token requirements](/stellar-contracts/tokens/confidential/confidential#underlying-token-requirements)
+and the [pooled-custody problem](/stellar-contracts/tokens/confidential/clawback#the-pooled-custody-problem) behind
+clawback also come from wrapping.
+
+## Who Sees What
+
+| Party | What it learns |
+| --- | --- |
+| **Anyone** | Every address involved in every operation, which operation it was and when it ran, the amounts of deposits, withdrawals and clawbacks, every delegation and its expiry, and the `auditor_id` each account chose. |
+| **The holder** | Everything about its own account: both balances, every amount it sent or received, and the allowances it granted. |
+| **A counterparty** | The amount of each transfer between the two accounts. Never the other account's balance. |
+| **A spender** | Its own allowance and the amounts it sends from it. Never the owner's spendable balance. |
+| **The account's auditor** | The amount of every transfer the account sends or receives and, from the account's first spend onward, its balances and allowances. See [Auditing](/stellar-contracts/tokens/confidential/auditing). |
+| **A viewing-key holder** | Everything the holder's wallet can read, including the openings of every transfer the account originated. It cannot spend. |
+| **A disclosure recipient** | Only the fact a [selective disclosure](/stellar-contracts/tokens/confidential/selective-disclosure) proof attests to. |
+
+An auditor learns about an account it doesn't audit only through that account's transfers with accounts it does.
+
+## What Each Event Reveals
+
+Events and invocation arguments are public. `Transfer`, `Withdraw`, `SetSpender` and `SpenderTransfer` also carry an
+ephemeral public key and a salt, which let the intended readers decrypt and reveal nothing on their own.
+
+| Event | Public | Encrypted | Readable by |
+| --- | --- | --- | --- |
+| `Register` | `account`, `auditor_id` | | |
+| `Deposit` | `from`, `to`, `amount` | | |
+| `Merge` | `account` | | |
+| `Transfer` | `from`, `to` | The amount | Recipient, both auditors |
+| | | The sender's new balance | Sender, sender's auditor |
+| `Withdraw` | `from`, `to`, `amount` | The sender's new balance | Sender, sender's auditor |
+| `SetSpender` | `account`, `spender`, `live_until_ledger` | The owner's new balance | Owner, owner's auditor |
+| | | The escrowed amount | Owner's auditor |
+| `SpenderTransfer` | `spender`, `from`, `to` | The amount | Recipient, both auditors |
+| | | The remaining allowance | Owner's auditor |
+| `RevokeSpender` | `account`, `spender` | The returned allowance | Owner, spender |
+| `Clawback` | `account`, `amount`, `destination` | | |
+| `Frozen`, `Unfrozen` | `account` | | |
+
+`SetSpender` and `SpenderTransfer` also pass the allowance encrypted for the owner and the spender (`a_tilde`) as a
+call argument and write it to the delegation's storage, so both can read the escrowed amount and the remaining
+allowance there. `RevokeSpender` republishes that ciphertext. The owner's auditor can't decrypt it, but already knows
+the returned allowance from the last `SetSpender` or `SpenderTransfer`.
+
+`Clawback`, `Frozen` and `Unfrozen` exist only in deployments with the
+[compliance extension](/stellar-contracts/tokens/confidential/compliance).
+
+Contract storage is public too. `confidential_balance` returns an account's public keys, commitments and
+`auditor_id`, and `get_spender_delegation` returns a delegation's expiry. With the compliance extension, `is_frozen`
+and `clawback_nonce` return whether an account is frozen and how many seizures it has been through.
+
+## Zero-Knowledge Setting
+
+Proofs are invocation arguments, so they are public too. The on-chain verifier implements only the
+non-zero-knowledge flavor of UltraHonk, so proofs are generated without the zero-knowledge setting. That leaves
+soundness intact but drops the guarantee that a proof hides its witness. The zero-knowledge flavor masks every
+committed witness polynomial with fresh randomness, so a proof reveals nothing about its witness by construction. The
+non-zero-knowledge flavor commits to the witness as it is, so a proof is a deterministic function of the witness and
+carries no such guarantee.
+
+The circuits close one known channel. Without masking, lookup read counts and read tags are committed in the clear,
+so a lookup indexed by a secret would make public which table rows it hit. Barretenberg multiplies by a constant
+base point with fixed-base lookup tables when that point is one of its precomputed generators, as the Pedersen
+generators `G` and `H` are, and the rows hit would follow windows of the secret scalar. The circuits therefore pass
+the generators to every scalar multiplication as witnesses, which moves it to the variable-base path. CI rejects any
+scalar multiplication with a constant base point and any circuit operation outside a list of those Barretenberg
+builds without lookup tables.
+
+This closes the lookup channel only; it does not make proofs private. Whether other parts of a non-zero-knowledge
+proof, such as the wire commitments or the opened evaluations, reveal private values has not been established. One
+case is known: the proof's commitment to its lookup inverses and that polynomial's opened evaluation determine two
+points in BN254, the curve the proof system works over, whose discrete logarithm is the prover's long-term key.
+Recovering the key from them is a discrete-logarithm problem in BN254, estimated at about 100 bits of security. That
+is the level the proof system's soundness already relies on, but below the roughly 127 bits that protect the key
+behind its public key on Grumpkin.
+
+Zero knowledge requires the zero-knowledge flavor of the verifier, which Nethermind has
+[in progress](https://github.com/NethermindEth/rs-soroban-ultrahonk/pull/38). Adopting it changes the proving
+recipe and the verifier dependency, not the circuits or the verification keys. See the package
+[README](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/README.md#zero-knowledge-setting)
+and [Generating Proofs](/stellar-contracts/tokens/confidential/proof-system#generating-proofs).
+
+## What Observers Can Infer
+
+Combining public events over time reveals more than any single event. The first two patterns come from amounts
+crossing the [boundary with the underlying token](#boundary-with-the-underlying-token); the others come from
+addresses being public:
+
+- **Balances built only from public flows are public.** Deposit and withdrawal amounts are public, and a merge only
+ moves value between an account's own balances. An account whose value has only entered through deposits and left
+ through withdrawals therefore has a balance anyone can compute. It becomes hidden once a confidential transfer, sent
+ or received, touches the account, and even then earlier flows bound it: an account known to hold 1,000 that sends a
+ transfer holds at most 1,000 afterwards.
+- **Matching amounts link flows.** A deposit of 1,234.56 into one account, a transfer from it to a second account,
+ and a withdrawal of 1,234.56 from the second strongly suggest what the transfer moved. The protocol cannot hide an
+ amount that appears in public on both sides.
+- **The transaction graph is public.** Every transfer names its sender and recipient, so observers can see who pays
+ whom, how often, and how many transfers an account receives. Each `SetSpender` publishes an owner-spender pair and
+ its expiry, and each `SpenderTransfer` names the owner it spends for.
+- **Timing reveals habits.** Merges, spends, and withdrawals that closely follow incoming transfers, or recur at fixed
+ intervals, show when a holder is active and can link accounts operated together.
+- **The auditor choice is public.** On a registry shared by several institutions, an account's `auditor_id` can hint
+ at whom the holder banks with or which jurisdiction it falls under.
+- **Transaction sources are public.** The Stellar transaction that carries an operation names a source account,
+ which pays the fee. When it differs from the account in the event, the two are linked.
+- **Compliance actions are public.** Freezes and clawbacks name the account. A clawback also publishes its
+ destination and, because it crosses to the underlying token, its amount.
+
+## Off-Chain Visibility
+
+The chain is not the only place where an account's activity can be seen:
+
+- **Queries.** A wallet or auditor that asks an RPC provider or an indexer for one account's events tells it which
+ account it follows, from which network address, and when. Fetching the whole contract stream and filtering
+ locally, or running one's own node and archive, avoids this. See
+ [Trust Model](/stellar-contracts/tokens/confidential/indexing#trust-model).
+- **Client software.** Keys, decrypted amounts, balances and openings that reach logs, analytics or crash reports
+ are beyond what the protocol can protect.
+- **Disclosed values.** Once a disclosure recipient decrypts a value, protecting it is the recipient's
+ responsibility.
+
+## Improving Privacy
+
+Most of these patterns are under the control of the holder and its wallet; see
+[Best Practices for Account Holders](/stellar-contracts/tokens/confidential/best-practices/account-holders). What a
+deployment can do for its users is covered in
+[Best Practices for Administrators](/stellar-contracts/tokens/confidential/best-practices/administrators).
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..b079cda3
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/proof-system.mdx
@@ -0,0 +1,288 @@
+---
+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 circuits are written in [Noir](https://noir-lang.org/) and compiled to UltraHonk, and the
+wallet generates proofs on the client. 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).
+That verifier supports only the non-zero-knowledge flavor of UltraHonk, so the circuits themselves are
+built to keep amounts and balances private; see [Generating Proofs](#generating-proofs).
+
+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);
+ }
+
+ // Shares the `manager` role with registration purely for illustration. See
+ // "Updating a Verification Key" for what a production deployment should do instead.
+ #[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 {}
+```
+
+The registry depends on `stellar-confidential`, like the token. See
+[Installation](/stellar-contracts/tokens/confidential/confidential#installation).
+
+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:
+
+| `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) |
+
+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);
+
+ // On a live network, the manager signs this same call with the `.vk.bin` contents.
+ 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));
+}
+```
+
+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 the settings below:
+
+```bash
+nargo execute --package circuit_
+# --oracle_hash keccak is required: the on-chain verifier reproduces the Fiat-Shamir
+# transcript with Keccak, while bb defaults to Poseidon2.
+# Don't pass --zk: the on-chain verifier implements only the non-zero-knowledge flavor.
+bb prove -s ultra_honk --oracle_hash keccak \
+ -b target/circuit_.json -w target/.gz -o
+```
+
+The [verifier's README](https://github.com/NethermindEth/rs-soroban-ultrahonk/blob/9aae9581eab7bc4c69209c0322e54e119ed3180e/crates/ultrahonk-soroban-verifier/README.md)
+for release 0.1.0 warns that such a proof carries witness-dependent messages and gives no
+witness-hiding guarantee, so private inputs stay hidden only as far as the circuit hides them. The
+confidential circuits are built for that setting, with one residual effect on the prover's long-term key.
+[Zero-Knowledge Setting](/stellar-contracts/tokens/confidential/privacy-model#zero-knowledge-setting)
+explains both.
+
+
+Privacy depends on the circuits keeping witness data out of lookup tables. Passing `G` or `H` to a
+scalar multiplication as a constant, for example, makes Barretenberg use its fixed-base lookup tables,
+and the proof then reveals which table rows the secret scalar's windows hit. Anyone modifying the
+circuits must keep
+[`check_witness_lookups.sh`](https://github.com/OpenZeppelin/stellar-contracts/blob/main/packages/confidential/circuits/scripts/check_witness_lookups.sh) passing.
+
+
+The proving recipe is provisional and 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).
+
+## 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..be13ae1a
--- /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 reveals 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 whole
+history. It is bound to the recipient's public key and a fresh nonce, so it can't be reused for a different request,
+and only that recipient can open its value. The recipient checks it against the event log, account records, and
+auditor registry, so it doesn't have to trust the prover. The prover can be the holder (with its viewing key) or the
+auditor (with its auditor key), and nothing about the contract's storage, entry points, or ciphertexts changes.
+
+**Non-goals.** The layer proves positive statements only ("this event paid me X"), never completeness ("there are no
+other transfers from Y"), which still goes through the auditor. Disclosures are not logged on-chain, since
+that would reveal 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..e560e568
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/spenders.mdx
@@ -0,0 +1,182 @@
+---
+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
+
+A delegation is active from `set_spender` until the ledger passes `live_until_ledger`, and expired
+after that. Expiry only stops spending: the remaining allowance stays escrowed in the delegation entry
+until `revoke_spender` deletes the entry and returns the allowance to the owner's spendable balance. In
+deployments with the compliance extension, the token admin can also end a frozen account's delegation
+with `force_revoke_spender` (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.
+
+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.
+
+## Setting a Spender
+
+```rust
+// `set_spender_data` is the XDR-encoded `SetSpenderData` built by the owner's wallet.
+// `spender` must already be registered. Moves the allowance out of the owner's spendable
+// balance into escrow.
+token_client.set_spender(&owner, &spender, &live_until_ledger, &set_spender_data);
+```
+
+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.
+
+## Spending from an Allowance
+
+```rust
+// Authorized by the spender, not the owner. Reverts with `DelegationNotFound` if there is
+// no delegation, and with `DelegationExpired` once the ledger is past `live_until_ledger`.
+token_client.confidential_transfer_from(&spender, &owner, &recipient, &spender_transfer_data);
+```
+
+The owner approved in advance at `set_spender`, and that approval lives in the delegation entry until
+it expires or is revoked.
+
+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 the salt of its current state,
+ `allowance_salt`, which only opens the current allowance. The spender's wallet picks a new salt,
+ `sigma_a_new`, keys every encryption mask to it, and the contract stores it as the new
+ `allowance_salt`. A reverted call leaves the stored salt unchanged, so the wallet must pick a fresh
+ `sigma_a_new` for every attempt, retries included. The circuit rejects only a value equal to the
+ stored salt; it can't enforce freshness.
+
+## Revoking a Spender
+
+```rust
+// Needs no proof. Adds the remaining allowance commitment back to the owner's spendable
+// commitment and deletes the delegation entry, whether active or expired.
+token_client.revoke_spender(&owner, &spender);
+```
+
+The `RevokeSpender` event carries the encrypted allowance and its salt. Because the call deletes the
+entry, the event is the only place the owner's wallet can read 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
+
+The spender can call `confidential_transfer_from` while `ledger.sequence() <= live_until_ledger`. After
+that, spending reverts, but the escrow stays in the delegation entry until the owner revokes it (see
+[Lifecycle](#lifecycle)).
+
+`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.
+
+## 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
+
+| 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..f4ed80fa
--- /dev/null
+++ b/content/stellar-contracts/tokens/confidential/wallet-integration.mdx
@@ -0,0 +1,208 @@
+---
+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 Wallet Matters
+
+A balance is a commitment `C = v·G + r·H`. The chain stores the point, but the opening `(v, r)` needed to spend it
+lives only in the wallet. If the wallet loses the opening or adds it up wrong, the funds can't be spent, and the
+contract can't help because it never knew the value. Every amount is decrypted by the client, so a decryption bug
+shows a plausible wrong number rather than an error. And all keys, blinding factors, and salts come from the client,
+so confidentiality is only as strong as 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.
+ [Generating Proofs](/stellar-contracts/tokens/confidential/proof-system#generating-proofs) has the commands and
+ the recipe's current status. 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)). The operational practices for
+each role are in Best Practices, for
+[account holders](/stellar-contracts/tokens/confidential/best-practices/account-holders) and
+[auditors](/stellar-contracts/tokens/confidential/best-practices/auditors).
+
+## Requirements
+
+- **Secrets stay local.** The root, keys, ephemeral scalars, and cached openings never leave the device, except under
+ opted-in remote proving.
+- **Nothing sensitive is logged.** Secrets, witness material, decrypted amounts, balances, and openings never reach
+ logs, telemetry, analytics, error reports, or crash dumps.
+- **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 has to hand `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..08dc5ead 100644
--- a/src/navigation/stellar.json
+++ b/src/navigation/stellar.json
@@ -67,6 +67,106 @@
"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": "page",
+ "name": "Privacy Model",
+ "url": "/stellar-contracts/tokens/confidential/privacy-model"
+ },
+ {
+ "type": "page",
+ "name": "Proof System",
+ "url": "/stellar-contracts/tokens/confidential/proof-system"
+ },
+ {
+ "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": "page",
+ "name": "Selective Disclosure",
+ "url": "/stellar-contracts/tokens/confidential/selective-disclosure"
+ },
+ {
+ "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": "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": "folder",
+ "name": "Best Practices",
+ "children": [
+ {
+ "type": "page",
+ "name": "Account Holders",
+ "url": "/stellar-contracts/tokens/confidential/best-practices/account-holders"
+ },
+ {
+ "type": "page",
+ "name": "Administrators",
+ "url": "/stellar-contracts/tokens/confidential/best-practices/administrators"
+ },
+ {
+ "type": "page",
+ "name": "Auditors",
+ "url": "/stellar-contracts/tokens/confidential/best-practices/auditors"
+ }
+ ]
+ }
+ ]
}
]
},