Skip to content

feat(confidential): wrapper contract + compliance extension#735

Open
brozorec wants to merge 7 commits into
mainfrom
feat/confidential-wrapper
Open

feat(confidential): wrapper contract + compliance extension#735
brozorec wants to merge 7 commits into
mainfrom
feat/confidential-wrapper

Conversation

@brozorec
Copy link
Copy Markdown
Collaborator

@brozorec brozorec commented May 29, 2026

Summary

Adds the confidential token wrapper — a SEP-41-backed contract with confidential balances and proof-gated transfers — together with a deployer-configurable compliance layer.

The wrapper exposes eleven entry points (register / deposit / merge / withdraw / two transfer variants / four operator-management methods / read accessors) via the ConfidentialTokenWrapper trait. Confidential balances are stored as Pedersen commitments on Grumpkin; every state-changing op that consumes private state ships an UltraHonk proof that the wrapper verifies through a separate verifier contract, and every transfer emits dual auditor ciphertexts.

A Hooks lifecycle trait runs synchronously at each entry point between auth and storage mutation — the canonical extension point for compliance gates, audit mirroring, rate limiting, etc.

The new wrapper::compliance submodule layers four controls (per-account freezing, pluggable external policy, SAC authorized() passthrough, unregistered-deposit carve-out) on top of those hooks. It ships a turnkey ComplianceHooks (wire as type Hooks = ComplianceHooks;) and an admin trait ConfidentialCompliance for freeze/unfreeze/config rotation.

What's in this PR

  • packages/contract-utils/src/crypto/grumpkin.rs — adds scalar multiplication (Grumpkin::mul) on the affine API, with tests, needed by the wrapper's deposit commitment.
  • packages/tokens/src/confidential/wrapper/ — the new module:
    • mod.rsConfidentialTokenWrapper trait, Hooks trait + NoHooks, all errors / events / payload types.
    • storage.rs*_no_auth orchestration helpers, public-input assembly per DESIGN §7, cross-contract verifier/auditor calls.
    • test.rs — wrapper-level unit tests.
  • packages/tokens/src/confidential/wrapper/compliance/ — the compliance submodule:
    • mod.rsPolicy interface, ComplianceError, events, ComplianceHooks, ConfidentialCompliance trait.
    • storage.rsComplianceConfig, storage keys, *_no_auth admin helpers, gating primitives (gate_account / check_policy / check_sac).
    • test.rs — 29 tests covering the no-config short-circuit, freeze flow, policy gate, SAC passthrough, unregistered-deposit carve-out, config rotation, the ConfidentialCompliance trait via its generated client, and end-to-end hook dispatch through the wrapper.
  • docs — module-level ## Underlying Token Requirements section documenting the exact-transfer-semantics requirement on the wrapped SEP-41 token (SAC or OZ fungible are conformant); deposit_no_auth / withdraw_no_auth carry matching # Notes blocks.

Design choices worth reviewing

  • Per-account freezing in the wrapper, not the SAC. Freezes a confidential account directly via persistent storage so the wrapper's gates fire on register / transfer / withdraw without round-tripping through the SAC. SAC passthrough remains opt-in via sac_passthrough.
  • Pluggable Policy interface. A single registry contract can serve multiple wrappers — is_authorized(account, wrapper) receives the wrapper's own address.
  • Unregistered-deposit carve-out (COMPLIANCE.md §4). When permit_unregistered_deposit is set, on_deposit skips freeze + policy on a depositor with no confidential account, but still runs SAC passthrough (since the underlying SEP-41 transfer would fail anyway if the SAC has unauthorized them).
  • Admin trait setters have no default body. freeze / unfreeze / set_compliance_config accept admin: Address and require the contract author to override — a default would either double-auth under #[only_owner] / #[only_role] or leave the entry point ungated.
  • Storage TTL convention. Reads on Frozen(account) extend TTL; writes don't (per CLAUDE.md). Compliance config sits in instance storage.
  • Out of scope: §5 (pooled-custody clawback / seizure) is intentionally deferred.

Test plan

  • cargo test --package stellar-tokens — 607 passing (29 new in compliance).
  • cargo +nightly fmt --all -- --check — clean.
  • cargo clippy --release --locked --package stellar-tokens -- -D warnings — clean.
  • WASM release build per-package
  • Coverage threshold (cargo llvm-cov --workspace --fail-under-lines 90)
  • Manual review of: gate sequencing in ComplianceHooks::on_deposit (carve-out branch), public-input order in storage.rs against DESIGN §7, the "no default body" rationale in the ConfidentialCompliance trait docstring

Notes for reviewers

  • The wrapper depends on ConfidentialVerifier::verify_proof, whose UltraHonk backend is still unaudited — module docstring already carries the "Not Production Ready" warning.
  • The exact-transfer-semantics requirement is currently documented rather than enforced at runtime. A defense-in-depth alternative would be to measure the wrapper's own balance pre/post token.transfer and credit/debit by the delta. Happy to swap docs → runtime measurement if preferred.

Summary by CodeRabbit

  • New Features

    • Added Grumpkin cryptographic operations for elliptic curve calculations
    • Introduced confidential token wrapper enabling private transactions with homomorphic commitments
    • Added compliance framework supporting account freezing and external policy authorization
  • Tests

    • Added comprehensive test suites for cryptographic operations, wrapper functionality, and compliance features

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 513141aa-df6b-46b0-a9af-42a3ea13fa7d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR implements a complete confidential token wrapper contract system for Soroban, adding Grumpkin cryptographic primitives (generator constant and scalar multiplication), the core wrapper with account registration, deposits, withdrawals, transfers, and operator delegation, plus a compliance extension layer supporting policy-based authorization and account freezing, all backed by comprehensive test coverage.

Changes

Confidential Token Wrapper with Compliance

Layer / File(s) Summary
Grumpkin Cryptographic Primitives
packages/contract-utils/src/crypto/grumpkin.rs, packages/contract-utils/src/crypto/test/grumpkin.rs
Implements Grumpkin::generator() constant and Grumpkin::mul() double-and-add scalar multiplication with on-curve validation, enabling address compression and key derivation in the wrapper.
Workspace Dependencies
Cargo.toml, packages/tokens/Cargo.toml
Adds soroban-poseidon v26.0.0 to workspace and tokens package dependencies, enabling Poseidon2 hashing for address-to-field compression.
Wrapper Contract Interfaces & Traits
packages/tokens/src/confidential/wrapper/mod.rs, packages/tokens/src/confidential/mod.rs
Defines ConfidentialTokenWrapper contract trait with pluggable Hooks for lifecycle events, entry points (register/deposit/merge/withdraw/confidential_transfer/operator operations), error codes, constants, and event types.
Wrapper Storage Schema & State Transitions
packages/tokens/src/confidential/wrapper/storage.rs
Implements on-chain storage for confidential accounts, operator delegations, and operation payloads; provides query/mutation functions for all lifecycle operations with proof verification routing and Poseidon2-based address compression.
Wrapper Unit & Integration Tests
packages/tokens/src/confidential/wrapper/test.rs
Tests wrapper lifecycle (register, deposit, merge, withdraw, transfer, operator delegation) with mock verifier/auditor contracts, deterministic fixtures, and end-to-end XDR payload encoding.
Compliance Extension Architecture
packages/tokens/src/confidential/wrapper/compliance/mod.rs
Introduces ConfidentialCompliance trait with freeze/unfreeze/config rotation; defines ComplianceHooks implementing gated callbacks, ComplianceError enum, and compliance events.
Compliance Storage & Gating Functions
packages/tokens/src/confidential/wrapper/compliance/storage.rs
Implements ComplianceConfig data model (policy/passthrough/unregistered-deposit flags), frozen account tracking, and low-level gate helpers coordinating freeze checks with optional policy and SAC authorization.
Compliance Hook & Policy Tests
packages/tokens/src/confidential/wrapper/compliance/test.rs
Tests compliance gating including no-config short-circuit, freeze/unfreeze, policy allow/deny, SAC passthrough, unregistered-deposit carve-out, config rotation, and end-to-end hook dispatch verification.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • ozgunozerk
  • bidzyyys

Poem

🐰 A wrapper so clever, with secrets well kept,
Compliance gates standing where policies crept,
Grumpkin's swift scalar dance spins through the night,
Freezing the foes while the friends see the light,
Soroban's treasure now wrapped up just right! 🎁

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: introducing a confidential token wrapper contract and a compliance extension feature.
Description check ✅ Passed The description is comprehensive and covers the PR scope, design choices, test plan, and notes for reviewers, though it lacks explicit tracking of the required PR checklist items (Tests and Documentation).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/confidential-wrapper

Comment @coderabbitai help to get the list of available commands and usage tips.

@brozorec brozorec requested a review from bidzyyys May 29, 2026 14:53
@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 95.97070% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.66%. Comparing base (637c53a) to head (60d83e1).

Files with missing lines Patch % Lines
...ackages/tokens/src/confidential/wrapper/storage.rs 95.73% 20 Missing ⚠️
...ens/src/confidential/wrapper/compliance/storage.rs 96.49% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #735      +/-   ##
==========================================
- Coverage   96.71%   96.66%   -0.06%     
==========================================
  Files          67       69       +2     
  Lines        6798     7344     +546     
==========================================
+ Hits         6575     7099     +524     
- Misses        223      245      +22     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/tokens/src/confidential/wrapper/compliance/storage.rs (1)

59-67: ⚡ Quick win

Return the stored flag when extending frozen TTL.

This helper hardcodes true on key presence instead of returning the persisted bool, so it diverges from the repository’s required TTL-read pattern and bakes in an invariant that the storage value can never differ.

Suggested change
 pub fn is_frozen(e: &Env, account: &Address) -> bool {
     let key = ComplianceStorageKey::Frozen(account.clone());
-    if e.storage().persistent().get::<_, bool>(&key).is_some() {
+    if let Some(is_frozen) = e.storage().persistent().get::<_, bool>(&key) {
         e.storage().persistent().extend_ttl(&key, FROZEN_TTL_THRESHOLD, FROZEN_EXTEND_AMOUNT);
-        true
+        is_frozen
     } else {
         false
     }
 }

As per coding guidelines, "For library-owned persistent/temporary storage reads, extend TTL using pattern: if let Some(value) = e.storage().persistent().get::<_, T>(&key) { e.storage().persistent().extend_ttl(&key, FOO_TTL_THRESHOLD, FOO_EXTEND_AMOUNT); value } with argument order always (&key, TTL_THRESHOLD, EXTEND_AMOUNT)"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tokens/src/confidential/wrapper/compliance/storage.rs` around lines
59 - 67, The is_frozen helper currently hardcodes true when the Frozen key
exists; change it to read and return the persisted bool instead by using the
repository TTL-read pattern: in is_frozen, call
e.storage().persistent().get::<_,
bool>(&ComplianceStorageKey::Frozen(account.clone())) and if let Some(value) =
... { e.storage().persistent().extend_ttl(&key, FROZEN_TTL_THRESHOLD,
FROZEN_EXTEND_AMOUNT); value } else { false }, ensuring you reference
ComplianceStorageKey::Frozen, FROZEN_TTL_THRESHOLD and FROZEN_EXTEND_AMOUNT
exactly as in the diff.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/tokens/src/confidential/wrapper/compliance/storage.rs`:
- Around line 96-172: The _no_auth helpers currently perform state mutation and
emit events; change them to be pure state-mutators only: update signatures for
set_compliance_config_no_auth, freeze_no_auth, and unfreeze_no_auth to accept an
extra caller: &Address parameter (but do not perform auth or emit events),
remove the emit_compliance_config_changed / emit_frozen / emit_unfrozen calls
from these functions so they only perform storage operations and the
compliance_config check, and move the corresponding emit_* calls into the
authenticated entrypoint functions that invoke these helpers so events are
emitted by the high-level auth-bearing entrypoints.

In `@packages/tokens/src/confidential/wrapper/mod.rs`:
- Around line 552-558: Add two exported constants for instance-storage TTL
defaults: define INSTANCE_EXTEND_AMOUNT as 30 * DAY_IN_LEDGERS and
INSTANCE_TTL_THRESHOLD as INSTANCE_EXTEND_AMOUNT - DAY_IN_LEDGERS, and export
them (pub const) alongside ACCOUNT_EXTEND_AMOUNT and DELEGATION_EXTEND_AMOUNT so
integrators have canonical values; do not add any calls to
instance().extend_ttl()—just expose INSTANCE_EXTEND_AMOUNT and
INSTANCE_TTL_THRESHOLD as defaults next to DAY_IN_LEDGERS,
ACCOUNT_EXTEND_AMOUNT, ACCOUNT_TTL_THRESHOLD, DELEGATION_EXTEND_AMOUNT, and
DELEGATION_TTL_THRESHOLD.

In `@packages/tokens/src/confidential/wrapper/storage.rs`:
- Line 450: The storage layer function calling emit_register(e, account,
auditor_id) (and other emit_* calls in the same file) must stop emitting events;
remove all emit_* invocations from the *_no_auth helpers in
packages/tokens/src/confidential/wrapper/storage.rs and ensure those helpers
accept caller: &Address only as a parameter for potential event context but
perform no auth or event emission; then add corresponding emit_* calls into the
high-level trait entrypoints in packages/tokens/src/confidential/wrapper/mod.rs
so that the public trait methods perform auth and emit events after calling the
low-level _no_auth functions (apply the same change to the other occurrences
noted in the review: the helper functions around the ranges referenced so they
remain pure state/proof transitions and all event emission is moved up to the
trait implementations).
- Around line 1041-1043: The setters set_verifier (and the analogous set_auditor
at 1061-1063) currently overwrite existing values; change them to be write-once
like set_token/set_wrap by first checking
storage.instance().get(&WrapperStorageKey::Verifier) (and
WrapperStorageKey::Auditor) and rejecting the call if a value already exists
(return/panic with a clear error message) so repeated calls cannot replace the
verifier/auditor; if rotation is desired instead, implement an explicit
authenticated rotate_* flow with an event rather than allowing direct overwrite
here.

---

Nitpick comments:
In `@packages/tokens/src/confidential/wrapper/compliance/storage.rs`:
- Around line 59-67: The is_frozen helper currently hardcodes true when the
Frozen key exists; change it to read and return the persisted bool instead by
using the repository TTL-read pattern: in is_frozen, call
e.storage().persistent().get::<_,
bool>(&ComplianceStorageKey::Frozen(account.clone())) and if let Some(value) =
... { e.storage().persistent().extend_ttl(&key, FROZEN_TTL_THRESHOLD,
FROZEN_EXTEND_AMOUNT); value } else { false }, ensuring you reference
ComplianceStorageKey::Frozen, FROZEN_TTL_THRESHOLD and FROZEN_EXTEND_AMOUNT
exactly as in the diff.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 36b7c738-978c-4256-88c4-f3e051806b7c

📥 Commits

Reviewing files that changed from the base of the PR and between 637c53a and 0e59b3c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • Cargo.toml
  • packages/contract-utils/src/crypto/grumpkin.rs
  • packages/contract-utils/src/crypto/test/grumpkin.rs
  • packages/tokens/Cargo.toml
  • packages/tokens/src/confidential/mod.rs
  • packages/tokens/src/confidential/wrapper/compliance/mod.rs
  • packages/tokens/src/confidential/wrapper/compliance/storage.rs
  • packages/tokens/src/confidential/wrapper/compliance/test.rs
  • packages/tokens/src/confidential/wrapper/mod.rs
  • packages/tokens/src/confidential/wrapper/storage.rs
  • packages/tokens/src/confidential/wrapper/test.rs

Comment thread packages/tokens/src/confidential/wrapper/compliance/storage.rs
Comment thread packages/tokens/src/confidential/wrapper/mod.rs
Comment thread packages/tokens/src/confidential/wrapper/storage.rs
Comment thread packages/tokens/src/confidential/wrapper/storage.rs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant