Skip to main content

ADR-027: Public Swaps via EBSwap.atomicPublicSwap

StatusProposed
Date2026-07-10

Context

Users need to swap ordinary public ERC-20s, initially USDC, against encrypted ZKEMT balances in both directions:

  • public ERC-20 to encrypted ZKEMT; and
  • encrypted ZKEMT to public ERC-20.

These are RFQ exchanges between independently funded token contracts, not the existing same-token ZKEMT publicToEncryptedTransfer or encryptedToPublicTransfer operations. They do not mint, burn, or otherwise change issuance.

The RFQ model from ADR-024 should remain: the client signs an order off-chain, the market maker (MM) supplies the counter-leg, and the MM submits one atomic sponsored transaction. The public amount and addresses are visible; this feature does not try to conceal them.

EBSwap.atomicSwap currently settles two encrypted legs between same-Hub ZKEMTs. The public path must preserve that entry point's ABI, behavior, EIP-712 domain, and authorization type. The current MM submits fills through the ownerless SharedAccount, so msg.sender at the settler is not the MM treasury and cannot authorize a public payout.

Public-token authorization must therefore be gasless, bound to the settler, and paired with an intent signature that commits the payer to the encrypted counter-leg.

Proposal

Combined atomic settler

Add an explicit EBSwap.atomicPublicSwap entry point to the fixed, non-upgradeable EBSwap. It settles exactly one public ERC-20 leg and one encrypted ZKEMT leg; the public token may be any EIP-3009 token other than the swap's encrypted token, including another ZKEMT's public side. The existing private atomicSwap entry point remains unchanged. Both paths use one contract address, EIP-712 domain (name = "EBSwap", version = "1"), authorization nonce bitmap, and reentrancy guard. EBSwap has no administrator, order book, or liquidity inventory.

One direction-neutral PublicSwapIntent identifies the two assets, public payer and recipient, encrypted payer and recipient EPKs, public amount, encrypted amount ciphertext, expiry, and intent salt. The same entry point handles both directions; taker/maker direction remains an off-chain SDK and MM concern.

EBSwap.atomicPublicSwap:

  1. validates the public intent and authorization;
  2. reuses the private _spendLeg helper to validate and relay the caller-bound encrypted transfer;
  3. receives the public amount through EIP-3009 and forwards it to the public recipient; and
  4. emits the completed swap.

Either both legs execute or the transaction reverts. The contract temporarily holds the public amount during the call and returns to its pre-call public-token balance after successful settlement. It has no rescue function; tokens transferred to it outside atomicPublicSwap are unrecoverable.

receiveWithAuthorization requires the recipient to submit the authorization, so the public funds must first enter EBSwap. A bearer cannot execute that leg directly on the token, as it could with transferWithAuthorization, and leave the payer without the encrypted counter-leg. The settler forwards the funds only inside the atomic call.

Paired authorizations

Each economic payer supplies two signatures:

  • public payer: an EBSwap intent authorization plus EIP-3009 ReceiveWithAuthorization targeting EBSwap;
  • encrypted payer: an EBSwap intent authorization plus the existing ZKEMT encryptedTransferWithCallerAuth authorization with caller = EBSwap, binding the payer's chosen pending-balance flags.

Both intent authorizations preserve the existing primary type exactly: SwapIntentAuth(bytes32 intentHash,bytes32 transferParamsHash,uint256 nonce,uint256 deadline). For the public payer, transferParamsHash contains the EIP-3009 ReceiveWithAuthorization struct hash. For the encrypted payer, it contains keccak256(abi.encode(encryptedParams)). This prevents either payment authorization being paired with different economic terms. The shared EBSwap EIP-712 domain binds signatures to the chain and deployment.

The AuthorizationChecker bitmap is shared across private and public entry points, so intent-auth nonces must avoid cross-flow collisions for the same signer. The public token's EIP-3009 nonce bitmap is separate, except where a token such as ZKEMT internally shares it with its own authorization flows.

Settlement requires block.timestamp < intent.expiry. USDC's EIP-3009 authorization is already invalid when block.timestamp == validBefore; setting validBefore = intent.expiry makes this the effective boundary. The settler exposes that rule directly instead of using private atomicSwap's inclusive intent check. AuthorizationChecker also requires block.timestamp < deadline, so deadlines set to expiry reject equality on both paths.

The MM signs the public pair for encrypted-to-public fills and the encrypted pair for public-to-encrypted fills. The taker signs the opposite pair. Anyone may submit the completed call; in practice the MM submits it through the sponsored SharedAccount.

Public-token scope

The contract has no on-chain token allowlist. Supported public tokens are configured in the SDK and MM with address and EIP-712 name and version; the MVP validates that each uses six decimals.

EBSwap.atomicPublicSwap calls the EIP-3009 bytes-signature overload and does not reject code-bearing payers. The contract accepts arbitrary signature bytes. As an MVP admission policy, the SDK and MM accept only 65-byte signatures: code-free payers use ECDSA recovery, while code-bearing payers must expose an ERC-1271 implementation that accepts the same 65-byte signature. This supports compatible EIP-7702 delegates without claiming general contract-wallet signing support or requiring a later settler redeployment.

Settlement does not verify balance movement on the public leg. Exact-transfer semantics (no fees or rebasing) are an admission requirement enforced off-chain by the SDK and MM token configuration; on-chain balance-delta checks were considered and dropped, since they only matter for tokens the admission policy already excludes.

ERC-2612, Permit2, standing-allowance settlement, non-six-decimal tokens, and general contract-wallet signing UX are deferred. Non-65-byte ERC-1271 signatures can use the existing bytes-signature entry point when enabled. Other authorization schemes require a new typed entry point and fixed deployment.

SDK and market maker

The private-swap APIs and records remain unchanged. Public swaps use direction-specific SDK methods and MM wire variants, each accepting only the account, proof, and randomness it needs. Both paths reuse the existing ebSwap address/configuration and paymaster target; there is no second settler domain or configuration.

The existing MM controller key also owns the public treasury. This is an MM-only MVP exception to ADR-015, which recommends separating public and controller keys for privacy. The maker's identity is already public in this flow, but one key compromise can affect both inventories.

Public inventory uses ERC20.balanceOf(treasury) as its source of truth. Encrypted-to-public orders reserve public output in the database at submission; no public balance mirror or pending-balance machinery is added. The MM takes the per-token reservation lock before reading balanceOf and holds it through the reservation insert. Terminal transitions that release a reservation take the same lock. Public-to-encrypted output reservations aggregate with private-swap output reservations under the current transaction-scoped encrypted-liquidity lock.

The public worker follows the private worker's current claim, retry, and balance-accounting model. This feature does not add cross-worker execution serialization or change private-swap recovery. Hardening those shared mechanisms belongs in a separate change.

The existing rate provider prices both directions uniformly, including USDC to zkUSD. Same-economic-asset pairs use the same inventory-backed RFQ path. All MVP assets use six decimals, so current amount arithmetic is reused.

The feature implementation includes local sponsored integration tests. Because EBSwap is fixed, changed bytecode requires a fresh deployment and ebSwap address cutover; existing signatures remain bound to the original address. Production deployment, paymaster policy, generated deployment metadata, and hosted MM rollout are a separate operational change after the contract and service behavior are accepted.

Alternatives considered

  • Separate EBSwapPublic. Isolates arbitrary ERC-20 calls and the deployment cutover from the private settler, but duplicates the settler address, EIP-712 domain, configuration, authorization flow, and encrypted-leg validation. Rejected in favor of one typed EBSwap ABI and reused _spendLeg path.
  • Generic authorization enum plus opaque bytes. Makes later schemes pluggable through one ABI, but weakens type safety across Solidity, TypeScript, and Rust. Rejected. New typed entry points may be added when another scheme is actually required.
  • ERC-2612 or Permit2 now. EIP-3009 provides a settler-pinned pull and unordered nonces for concurrent MM payouts. ERC-2612 needs permit plus allowance consumption and uses an owner sequence; Permit2 requires prior approval. Neither improves the initial USDC flow.
  • Standing allowance only. Works for a pre-funded MM but is not gasless onboarding for a new taker. Deferred as a future compatibility path.
  • Compose a ZKEMT layer transfer with a private swap. Requires multiple non-atomic steps and does not uniformly support cross-asset pairs. Rejected in favor of one settlement transaction.
  • On-chain public-token allowlist. Requires an owner or a fixed list and adds policy state to the settler. Rejected; the MM decides which pairs it quotes.
  • Mirrored public balances. Public balances are cheap to read directly, unlike encrypted balances that require decryption and discrete-log recovery. Rejected; only reservations are persisted.
  • Separate public-treasury key. Better isolates key risk and follows ADR-015, but adds MVP operations and configuration. Deferred while the MM's public identity is already exposed.

Consequences

Easier:

  • Existing EBSwap.atomicSwap, ZKEMT, circuits, and private-swap APIs remain unchanged.
  • Existing circuits, ZKEMT state, and issuance semantics remain unchanged.
  • One EBSwap address, EIP-712 domain, ABI, configuration, and paymaster target serves both paths.
  • Public settlement reuses the encrypted-leg validation and execution path.
  • The client stays gasless when the public token supports EIP-3009.
  • The MM can use unordered EIP-3009 nonces and submit public payouts concurrently when they touch different encrypted balances.
  • Public inventory reconciliation is a direct balanceOf read.

Harder:

  • Adding external ERC-20 behavior broadens the EBSwap audit and reentrancy surface.
  • Private and public entry points share authorization nonce and reentrancy state, so cross-flow behavior and nonce selection must be tested.
  • The fixed EBSwap bytecode requires a fresh deployment and configuration cutover.
  • Public-to-encrypted takers can invalidate an accepted order by moving their public balance, causing one sponsored fill to revert.
  • The MM must reserve public payouts atomically in its database and keep its treasury from making untracked withdrawals.
  • Public amounts, addresses, and rates are visible, and the encrypted amount is inferable from the quote.
  • Reusing the maker controller key for the public treasury expands the impact of key compromise.
  • Unsupported or accidentally transferred tokens have no recovery path from the ownerless settler.
  • Until shared worker coordination is hardened separately, concurrent fills inherit the private worker's retry and reconciliation limits.

Unchanged:

  • The encrypted leg uses the existing transfer circuit and caller-bound ZKEMT authorization.
  • Recipient EPKs must be registered before the swap.
  • Quote authentication, rate-provider verification, short expiries, sponsored submission, and status polling follow the private RFQ model.