Skip to content

feat(mpp/charge): account-setup helper + TransferWithFee bundle#190

Open
lgalabru wants to merge 3 commits into
mainfrom
feat/confidential-setup-and-fee
Open

feat(mpp/charge): account-setup helper + TransferWithFee bundle#190
lgalabru wants to merge 3 commits into
mainfrom
feat/confidential-setup-and-fee

Conversation

@lgalabru

Copy link
Copy Markdown
Collaborator

Summary

Two additions to the Token-2022 confidential-transfer support in solana-mpp (confidential feature), needed to drive a confidential transfer end-to-end against a fee-bearing mint (USDPT) from a test environment.

1. derive_confidential_account_setup(signer, mint)crates/mpp/src/protocol/confidential.rs

Returns everything a test cheatcode needs to stand up a configured confidential account for an owner: the associated token account (Token-2022) plus the owner's ElGamal public key and AES key as raw bytes.

pub struct ConfidentialAccountSetup {
    pub token_account: Pubkey,
    pub elgamal_pubkey: [u8; 32],
    pub ae_key: [u8; 16],
}
pub async fn derive_confidential_account_setup(
    signer: &dyn SolanaSigner,
    mint: &Pubkey,
) -> Result<ConfidentialAccountSetup, Error>;

The keys are derived deterministically from the owner's wallet signature (via the existing derive_confidential_keys), so whatever fabricates the account encrypts to keys the owner can later re-derive and spend. This is what surfpool's new surfnet_setTokenAccount confidential option consumes (see references) and what the pay CLI's confidential setup calls.

2. TransferWithFee bundle support — crates/mpp/src/client/confidential.rs

A mint that carries the ConfidentialTransferFeeConfig extension (e.g. USDPT) requires the fee-bearing transfer variant — a plain confidential Transfer is rejected by the Token-2022 program (surfaces as InvalidInstructionData), even when the fee rate is 0%.

build_confidential_transfer_with_fee_bundle implements that path:

  • reads the mint's TransferFeeConfig (current-epoch rate + cap) and ConfidentialTransferFeeConfig (withdraw-withheld-authority ElGamal pubkey);
  • generates the five split proofs via transfer_with_fee_split_proof_data — equality, transfer-amount validity, fee sigma (percentage-with-cap), fee validity, and a U256 range proof (vs three for the plain transfer);
  • stages the U256 range proof in an spl-record account and emits inner_transfer_with_fee referencing the five proof-context accounts.

build_confidential_transfer_bundle now branches on whether the mint has a transfer-fee config; non-fee mints keep the lean 3-proof Transfer path unchanged.

Why

Without (1) there's no ergonomic way to bootstrap a confidential account for tests (the real flow is ConfigureAccount → Deposit → ApplyPendingBalance, 3+ txs needing the ZK ElGamal Proof program and owner signatures). Without (2), confidential transfers simply fail on any fee-bearing mint — which includes USDPT, the flagship confidential stablecoin.

Verified end-to-end on a local stack (surfpool forking the real USDPT mint + a local pay-api): a confidential TransferWithFee settled to a finalized signature with the recipient's pendingBalanceCreditCounter incrementing.

Notes

  • Backwards compatible: purely additive; the existing plain-transfer path is unchanged.
  • cargo build -p solana-mpp --features confidential ✓; cargo fmt --check ✓.

Related work (cross-repo)

  • surfpoolsurfnet_setTokenAccount confidential cheatcode that consumes derive_confidential_account_setup's output: solana-foundation/surfpool#705 (its description is a full ZK/confidential-transfer onboarding). Follow-up DX leads: surfpool#706. Docs: surfpool-web-ui#30.
  • pay (CLI) — pay confidential setup + confidential-aware send, on branch feat/confidential-send (depends on this PR).
  • pay-api (agent-gateway) — two settlement fixes surfaced during this work: the confidential worker must use the MPP cluster (localnet) not the config network key (sandbox), and the SOL/USD price oracle should be fetched lazily (skipped for confidential sends). Local-only so far; the cluster fix likely affects the hosted sandbox.
  • Reference: gitteri/confidential-balances-exploration (full reference impl) and the zkproof8-talk deck.

🤖 Generated with Claude Code

Two additions to the Token-2022 confidential-transfer support:

- `derive_confidential_account_setup(signer, mint)` (protocol/confidential.rs):
  returns the owner's associated token account plus ElGamal public key and AES
  key (as raw bytes), ready to hand to a test cheatcode — e.g. surfpool's
  `surfnet_setTokenAccount` — to fabricate a configured confidential account
  without the real configure/deposit/apply-pending-balance flow.

- TransferWithFee bundle support (client/confidential.rs): mints that carry the
  `ConfidentialTransferFeeConfig` extension (e.g. USDPT) require the fee-bearing
  transfer variant. `build_confidential_transfer_with_fee_bundle` generates the
  five split proofs (equality, transfer-amount validity, fee sigma /
  percentage-with-cap, fee validity, U256 range) and emits
  `inner_transfer_with_fee`. The builder reads the mint's transfer-fee config and
  branches to this path; non-fee mints keep the lean 3-proof `Transfer` path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lgalabru lgalabru changed the title feat(mpp/confidential): account-setup helper + TransferWithFee bundle feat(mpp/charge): account-setup helper + TransferWithFee bundle Jun 28, 2026
@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR expands confidential Token-2022 transfer support in the MPP kit. The main changes are:

  • Added a helper that derives a Token-2022 associated token account and confidential keys for setup flows.
  • Added fee-bearing confidential transfer bundle construction with the TransferWithFee instruction.
  • Added fee proof context creation, range proof staging, and cleanup for fee-bearing mints.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
rust/crates/kit/src/mpp/client/confidential.rs Adds the fee-bearing confidential transfer bundle path for Token-2022 mints with transfer fee extensions.
rust/crates/kit/src/mpp/protocol/confidential.rs Adds a setup helper that returns the Token-2022 associated account and derived confidential key bytes.

Reviews (3): Last reviewed commit: "chore: merge main into feat/confidential..." | Re-trigger Greptile

Comment thread rust/crates/kit/src/mpp/client/confidential.rs
Addresses review (Greptile "Fee Epoch Can Drift"): the fee parameters are
read for the current epoch and baked into the fee proofs. A mismatch with
the on-chain TransferWithFee check is only possible when the mint has a
*scheduled* fee-schedule change and the bundle is built in the last epoch
before it but lands after the rollover. That fails closed (transfer
rejected, never mis-settled) and is retriable; mints without a pending
change have no drift. Matches standard spl-token tooling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
// there is no drift. This matches standard spl-token tooling, which likewise reads
// the current epoch's fee at build time.
let epoch = rpc
.get_epoch_info()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Epoch Drift Still Fails The fee parameters are still read from get_epoch_info() when the bundle is built, and the returned bundle has no freshness check before the final TransferWithFee transaction runs. If the bundle is built in the last epoch before a scheduled transfer-fee change, the proof transactions can create contexts for the old fee schedule, then the final transfer can land after rollover and be verified by Token-2022 against the new fee schedule. That rejects the payment after the earlier bundle transactions have already run, so documenting the narrow case does not prevent the failed transfer.

@EfeDurmaz16 EfeDurmaz16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review summary — adversarial line-by-line pass

Verdict: no approve-blocking findings. Feature-gated (confidential) test-env helper plus the fee-bearing confidential-transfer builder; both sound.

  • derive_confidential_account_setup is safely test-scoped. Keys are deterministically re-derived from the owner's own wallet signature over the ATA seed (spl-token convention), never separately stored, and it exports only the owner's own ElGamal pubkey + AES key for a test cheatcode to fabricate a decryptable balance the same owner later spends. No third-party secret leaves the owner, and the whole path is behind the confidential feature.
  • TransferWithFee / fee-epoch handling is correct. Fee params are read for the current epoch and baked into the fee proofs; the on-chain TransferWithFee re-derives at execution. The single drift window (a scheduled fee-schedule change where the bundle is built pre-rollover and lands post-rollover) fails closed and is retriable by rebuild, and older == newer mints (USDPT) never drift. Pre-flight checks (credits/approval/balance) run before proof generation, and the litesvm tests verify the proofs against the real ZK program plus recipient/auditor recovery.

Blockers: none. Follow-ups: none.

EfeDurmaz16
EfeDurmaz16 previously approved these changes Jul 1, 2026

@EfeDurmaz16 EfeDurmaz16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve

Ready. Re-checked for completeness after the first pass:

  • CI is 41/41 green; no TODO/unimplemented!/stubs.
  • build_confidential_transfer_bundle (the TransferWithFee bundle) is wired into the production confidential-charge path (confidential_charge_payload, client/confidential.rs:749) and exported from client/mod.rs — not orphaned. litesvm tests verify the proofs against the real ZK program plus recipient/auditor recovery.
  • Key material stays owner-scoped and behind the confidential feature (verified earlier).

Optional follow-up (non-blocking): derive_confidential_account_setup has no in-repo caller and no direct unit test — by design its consumer is an out-of-repo test cheatcode (surfpool surfnet_setTokenAccount). A small smoke test asserting the derived ATA/keys round-trip with what the client re-derives would close the loop, but it's a documented primitive built from already-tested pieces, so not blocking.

LGTM.

@lgalabru

lgalabru commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jul 7, 2026

Copy link
Copy Markdown

@copilot resolve the merge conflicts in this pull request

Merged main into this branch and resolved the conflict in rust/crates/kit/src/mpp/client/confidential.rs. The conflict was a single import block where main had refactored the paths to crate::mpp::* — the resolution keeps those updated paths while also retaining the ConfidentialKeys import this PR added. cargo check -p solana-pay-kit --features confidential is clean.

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.

3 participants