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