You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The wrapper contract is the integration layer for the Confidential Token Wrapper: it holds SEP-41 tokens on behalf of users, manages confidential account state (spendable + receiving balance commitments), delegates proof verification to the verifier (#701), fetches auditor keys from the auditor contract (#702) at operation time, and emits events that drive the SDK and indexer. See internal design doc §3, §6, §7, §11 (link available in the tracking workspace).
This ticket covers the core (non-operator) path. Operator delegation lives in a separate Medium ticket so each piece can ship and audit independently. Core is where every other prerequisite ticket converges, so it depends on:
wrap = address_to_field(env.current_contract_address()) in instance storage, computed once at __constructor (design doc §2.7, §3.5). Read on every owner-initiated proof verification.
State-changing entry points
register(account, auditor_id, data) — verify Register proof (Multi-VK UltraHonk verifier contract for confidential token wrapper #701, CircuitType::Register); validate auditor_id exists on the auditor contract via get_key; reject if account is already registered (§11.1, single-use); initialize spendable_balance = receiving_balance = O. Emit Register.
deposit(from, to, amount) — reject amount < 0 (§3.4); SEP-41 token.transfer(from, self, amount); C_receive += a · G via the Grumpkin crate (On-chain Grumpkin point arithmetic for Soroban wrapper #700). Recipient to must be registered (§7.3). Emit Deposit.
merge(account) — require_auth; C_spend ← C_spend + C_receive; reset C_receive ← O. No proof. Emit Merge.
withdraw(from, to, amount, data) — reject amount < 0; verify Withdraw proof; SEP-41 token.transfer(self, to, amount); update C_spend. Emit Withdraw with (from, to, amount, R_e, σ, b̃, b̃_aud_s) (§11.2).
confidential_transfer(from, to, data) — verify Transfer proof; update sender's C_spend; add C_tx to recipient's C_receive. Emit Transfer with (from, to, R_e, ṽ, σ, b̃, ṽ_aud_r, r̃_aud_r, ṽ_aud_s, b̃_aud_s) (§11.2 — the recipient-auditor r̃_aud_r ciphertext is the one that lets the recipient-auditor reconstruct C_receive's opening, §8.1).
Read view
confidential_balance(account) -> Bytes — returns the XDR-serialized ConfidentialAccount struct (§11.3); reverts if not registered.
Auditor-key fetch atomicity (security-critical)
For every op that produces auditor ciphertexts (withdraw, transfer), the flow is: read each account's auditor_id → call auditor.get_key(auditor_id) → assemble public inputs (including the fetched key) → call verifier.verify_proof(...). This sequence must be a single host-call boundary with no caller-supplied path between key fetch and proof verification. A race between key rotation on the auditor contract and verification here would let a stale or attacker-substituted key pass into the proof; if rotation occurs mid-flight, UltraHonk verification fails and the invocation reverts cleanly (§8.3, in-flight proofs across rotation). Treat the key-fetch helper as a security-critical primitive; cover it in dedicated tests. The operator-path ticket reuses this same helper.
Trust-boundary discipline (§7.1)
Public inputs that derive from trusted state (account storage, the wrapper's stored wrap, auditor-contract lookups) MUST be loaded by the wrapper itself. The wrapper MUST NOT accept these values from the caller's data payload. The data schema per op is fixed by design doc §11; only prover-supplied values travel in data.
Event emission utilities
Centralize event construction in a single module so the exact field set per event type (design doc §11.2) is enforced by the type system, not by ad-hoc per-handler code. The indexer (separate ticket) and SDK both contract on this schema. Operator events (added in the operator-path ticket) extend the same module.
End-to-end tests
Happy path (core): register two accounts → deposit to A → merge A → A confidential_transfer to B → B merges → B withdraws.
Negative: replay an already-verified proof (must fail because C_spend no longer matches — design doc §9.7); third-party spam transfers to A do not invalidate A's in-flight spend proof (§9.1); operations referencing an unknown auditor_id are rejected; register on an already-registered account reverts (§11.1); deposit/withdraw with negative amount revert (§3.4).
Out of scope: operator delegation (separate ticket); compliance extensions; the indexer; SDK work.
Key Questions
How are XDR-encoded data payloads laid out per op (design doc §11 table)? Must match the SDK's encoder byte-for-byte; lock this with the SDK ticket before merge.
Proposed Approach
Build entry-point-by-entry-point, with the auditor-key fetch helper landing first so every proof-bearing op consumes the same primitive. The operator-path ticket builds on this foundation.
Define storage schema for ConfidentialAccount. Add accessors and the event-emission module (extensible — operator events plug in later).
Build the auditor-key fetch helper: fetch_auditor_keys(account_ids: &[u32]) -> Vec<Point>. Single host-call boundary, validates returned points via is_on_curve + is_not_identity (cheap defense-in-depth even though the auditor contract validates on write). Dedicated tests for race / substitution scenarios.
Implement merge (no proof) and withdraw. Verify spend-proof stability under concurrent deposits (§9.1).
Implement confidential_transfer. Wire the dual-auditor key fetch (recipient's + sender's auditor_id). E2E test: A → B end-to-end with B then withdrawing.
Implement the negative test suite called out in Scope. Each negative test references the design doc proposition it validates.
Document the per-op gas envelope in the PR description; flag any op that breaches.
Known References
Internal design doc — §3 (System Model), §3.4 (Underlying Token Assumptions), §3.5 (Governance and Upgradeability — wrap in instance storage), §6.1 (Account Data Model), §7.1 (Trust-boundary rule), §7.2 (Registration), §7.3 (Deposit), §7.4 (Merge), §7.5 (Withdrawal), §7.6 (Confidential Transfer), §8.3 (Auditor Key Rotation), §9 (Security Analysis), §11 (Interface), §11.1 (Authorization Model), §11.2 (Event Schema), §11.3 (Read Methods). Link available in the tracking workspace.
Context
The wrapper contract is the integration layer for the Confidential Token Wrapper: it holds SEP-41 tokens on behalf of users, manages confidential account state (spendable + receiving balance commitments), delegates proof verification to the verifier (#701), fetches auditor keys from the auditor contract (#702) at operation time, and emits events that drive the SDK and indexer. See internal design doc §3, §6, §7, §11 (link available in the tracking workspace).
This ticket covers the core (non-operator) path. Operator delegation lives in a separate Medium ticket so each piece can ship and audit independently. Core is where every other prerequisite ticket converges, so it depends on:
get_key(auditor_id)lookup per operation that produces auditor ciphertexts.Scope
Build
contracts/wrapper/matching the core (non-operator) surface of design doc §11.Storage
ConfidentialAccountkeyed byAddress(design doc §6.1):spending_key,viewing_public_key,spendable_balance,receiving_balance,auditor_id.wrap = address_to_field(env.current_contract_address())in instance storage, computed once at__constructor(design doc §2.7, §3.5). Read on every owner-initiated proof verification.State-changing entry points
register(account, auditor_id, data)— verify Register proof (Multi-VK UltraHonk verifier contract for confidential token wrapper #701,CircuitType::Register); validateauditor_idexists on the auditor contract viaget_key; reject ifaccountis already registered (§11.1, single-use); initializespendable_balance = receiving_balance = O. EmitRegister.deposit(from, to, amount)— rejectamount < 0(§3.4); SEP-41token.transfer(from, self, amount);C_receive += a · Gvia the Grumpkin crate (On-chain Grumpkin point arithmetic for Soroban wrapper #700). Recipienttomust be registered (§7.3). EmitDeposit.merge(account)—require_auth;C_spend ← C_spend + C_receive; resetC_receive ← O. No proof. EmitMerge.withdraw(from, to, amount, data)— rejectamount < 0; verify Withdraw proof; SEP-41token.transfer(self, to, amount); updateC_spend. EmitWithdrawwith(from, to, amount, R_e, σ, b̃, b̃_aud_s)(§11.2).confidential_transfer(from, to, data)— verify Transfer proof; update sender'sC_spend; addC_txto recipient'sC_receive. EmitTransferwith(from, to, R_e, ṽ, σ, b̃, ṽ_aud_r, r̃_aud_r, ṽ_aud_s, b̃_aud_s)(§11.2 — the recipient-auditorr̃_aud_rciphertext is the one that lets the recipient-auditor reconstructC_receive's opening, §8.1).Read view
confidential_balance(account) -> Bytes— returns the XDR-serializedConfidentialAccountstruct (§11.3); reverts if not registered.Auditor-key fetch atomicity (security-critical)
For every op that produces auditor ciphertexts (withdraw, transfer), the flow is: read each account's
auditor_id→ callauditor.get_key(auditor_id)→ assemble public inputs (including the fetched key) → callverifier.verify_proof(...). This sequence must be a single host-call boundary with no caller-supplied path between key fetch and proof verification. A race between key rotation on the auditor contract and verification here would let a stale or attacker-substituted key pass into the proof; if rotation occurs mid-flight, UltraHonk verification fails and the invocation reverts cleanly (§8.3, in-flight proofs across rotation). Treat the key-fetch helper as a security-critical primitive; cover it in dedicated tests. The operator-path ticket reuses this same helper.Trust-boundary discipline (§7.1)
Public inputs that derive from trusted state (account storage, the wrapper's stored
wrap, auditor-contract lookups) MUST be loaded by the wrapper itself. The wrapper MUST NOT accept these values from the caller'sdatapayload. Thedataschema per op is fixed by design doc §11; only prover-supplied values travel indata.Event emission utilities
Centralize event construction in a single module so the exact field set per event type (design doc §11.2) is enforced by the type system, not by ad-hoc per-handler code. The indexer (separate ticket) and SDK both contract on this schema. Operator events (added in the operator-path ticket) extend the same module.
End-to-end tests
confidential_transferto B → B merges → B withdraws.C_spendno longer matches — design doc §9.7); third-party spam transfers to A do not invalidate A's in-flight spend proof (§9.1); operations referencing an unknownauditor_idare rejected;registeron an already-registered account reverts (§11.1);deposit/withdrawwith negativeamountrevert (§3.4).Out of scope: operator delegation (separate ticket); compliance extensions; the indexer; SDK work.
Key Questions
datapayloads laid out per op (design doc §11 table)? Must match the SDK's encoder byte-for-byte; lock this with the SDK ticket before merge.Proposed Approach
Build entry-point-by-entry-point, with the auditor-key fetch helper landing first so every proof-bearing op consumes the same primitive. The operator-path ticket builds on this foundation.
contracts/wrapper/. Wire in dependencies:lib/grumpkin/(On-chain Grumpkin point arithmetic for Soroban wrapper #700), SEP-41 token client, verifier client (Multi-VK UltraHonk verifier contract for confidential token wrapper #701), auditor client (Auditor contract for confidential token wrapper key management #702). Compute and storewrapin instance storage in__constructor(§3.5).ConfidentialAccount. Add accessors and the event-emission module (extensible — operator events plug in later).fetch_auditor_keys(account_ids: &[u32]) -> Vec<Point>. Single host-call boundary, validates returned points viais_on_curve+is_not_identity(cheap defense-in-depth even though the auditor contract validates on write). Dedicated tests for race / substitution scenarios.registeranddeposit. Add an integration test that exercises both against a deployed verifier with a real Register VK from Noir circuits for confidential token wrapper (epic) #703.merge(no proof) andwithdraw. Verify spend-proof stability under concurrent deposits (§9.1).confidential_transfer. Wire the dual-auditor key fetch (recipient's + sender'sauditor_id). E2E test: A → B end-to-end with B then withdrawing.Known References
wrapin instance storage), §6.1 (Account Data Model), §7.1 (Trust-boundary rule), §7.2 (Registration), §7.3 (Deposit), §7.4 (Merge), §7.5 (Withdrawal), §7.6 (Confidential Transfer), §8.3 (Auditor Key Rotation), §9 (Security Analysis), §11 (Interface), §11.1 (Authorization Model), §11.2 (Event Schema), §11.3 (Read Methods). Link available in the tracking workspace.Expert Brief Architecture
Acceptance Criteria