Skip to main content

ADR-005: SharedAccount for L3 Graph Privacy Without a Relayer

StatusProposed
Date2026-03-11

Context

L3 operations (unstake, merge) require graph privacy — an observer should not be able to link an on-chain transaction to a specific user. In the current design, a custom relayer submits L3 transactions on behalf of users, hiding their identity behind the relayer's EOA. This introduces liveness dependency, state management complexity (IMT), and a single point of failure.

With L2 gas sponsorship moving to ERC-4337 (see ADR-002), the question arises: can L3 also eliminate the relayer by leveraging account abstraction?

The challenge: if each user submits their own UserOperation from their own account (EOA or smart wallet), the msg.sender / account address links them to the L3 action — defeating graph privacy.

Proposal

Deploy a single SharedAccount — an ownerless ERC-4337 smart account that anyone can use to submit L3 transactions. All L3 operations (unstake, merge) go through this shared account, so on-chain they all appear to come from the same msg.sender. The ZK proof guarantees correctness; the account doesn't need to authorize anything beyond validating the calldata.

How it works:

  1. User generates ZK proof locally (unstake, merge, etc.)
  2. User builds a UserOperation with sender = SharedAccount
  3. The nonce key is derived from keccak256(callData || paymasterExtraData), ensuring different users with different proofs get unique nonce keys — no conflicts
  4. The paymaster sponsors gas (SharedAccount holds no assets)
  5. A public bundler submits the UserOperation
  6. On-chain: SharedAccount calls EBEMT.unstake(proof, publicInputs) — observer sees the SharedAccount address, not the user

Validation logic:

The SharedAccount has no owner and performs no signature check. Instead, validateUserOp verifies that the nonce key matches the hash of the calldata (+ paymaster extra data). This ensures:

  • No nonce collisions between concurrent users (each proof produces a unique calldata hash)
  • No replay (EntryPoint enforces nonce uniqueness)
  • No authorization needed (the ZK proof is the authorization — the EBEMT contract verifies it)

Production hardening (not in the research prototype):

  • Restrict execute() to only call allowed contracts (EBEMT) and allowed selectors (unstake, merge)
  • Rate limiting via the paymaster (not the account)
  • Consider whether the bundler seeing the UserOperation (before inclusion) is an acceptable metadata leak

Consequences

What becomes easier:

  • No relayer server to maintain — L3 is fully client-side like L2
  • No state management (no IMT, no nullifier tree sync)
  • No liveness dependency — if the SharedAccount is deployed, L3 works
  • Composes with the same ERC-4337 paymaster and bundler used for L2
  • Concurrency is solved by the nonce key scheme — no serialization bottleneck

What becomes harder:

  • The SharedAccount is a new on-chain contract to deploy and audit
  • Bundler/paymaster see L3 UserOperations in the mempool before inclusion (IP-level metadata leak, same as L2)
  • Need to ensure the EBEMT contract accepts calls from the SharedAccount address (access control)

What stays the same:

  • ZK proof generation is still local and client-side
  • The EBEMT contract's unstake/merge verification logic is unchanged
  • Privacy properties: amounts hidden, graph hidden (via SharedAccount), same as before