ADR-005: SharedAccount for L3 Graph Privacy Without a Relayer
| Status | Proposed |
| Date | 2026-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:
- User generates ZK proof locally (unstake, merge, etc.)
- User builds a UserOperation with
sender = SharedAccount - The nonce key is derived from
keccak256(callData || paymasterExtraData), ensuring different users with different proofs get unique nonce keys — no conflicts - The paymaster sponsors gas (SharedAccount holds no assets)
- A public bundler submits the UserOperation
- 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